feat: 优化视图组件与数据迁移逻辑
- 移除DataMigrationManager类,简化数据迁移逻辑。 - 在FeedListView和MeView中新增图片预览功能,提升用户体验。 - 更新OptimizedDynamicCardView以支持图片点击回调,增强交互性。 - 新增PreviewItem结构体以管理图片预览状态,提升代码可读性与维护性。 - 清理AppDelegate中的冗余代码,优化启动流程。
This commit is contained in:
@@ -9,6 +9,15 @@
|
|||||||
"version" : "1.0.3"
|
"version" : "1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "liquidglass",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/BarredEwe/LiquidGlass.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d5bf927a08a97c2d94db7ef71f1e15f8532d1005",
|
||||||
|
"version" : "0.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-case-paths",
|
"identity" : "swift-case-paths",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
@@ -16,13 +16,15 @@ let package = Package(
|
|||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.20.2"),
|
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.20.2"),
|
||||||
.package(url: "https://github.com/pointfreeco/swift-case-paths.git", branch: "main")
|
.package(url: "https://github.com/pointfreeco/swift-case-paths.git", branch: "main"),
|
||||||
|
.package(url: "https://github.com/BarredEwe/LiquidGlass.git", from: "0.7.0")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "yana",
|
name: "yana",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
|
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
|
||||||
|
"LiquidGlass"
|
||||||
],
|
],
|
||||||
path: "yana",
|
path: "yana",
|
||||||
),
|
),
|
||||||
|
@@ -88,7 +88,7 @@
|
|||||||
"location" : "https://github.com/pointfreeco/swift-navigation",
|
"location" : "https://github.com/pointfreeco/swift-navigation",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "ae208d1a5cf33aee1d43734ea780a09ada6e2a21",
|
"revision" : "ae208d1a5cf33aee1d43734ea780a09ada6e2a21",
|
||||||
"version" : "2.3.1"
|
"version" : "2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -3,84 +3,10 @@ import UIKit
|
|||||||
|
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
private func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) async -> Bool {
|
private func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) async -> Bool {
|
||||||
|
|
||||||
// isPerceptionCheckingEnabled = false
|
|
||||||
|
|
||||||
// 执行数据迁移(从 UserDefaults 到 Keychain)
|
|
||||||
DataMigrationManager.performStartupMigration()
|
|
||||||
|
|
||||||
// 预加载用户信息缓存
|
// 预加载用户信息缓存
|
||||||
await UserInfoManager.preloadCache()
|
await UserInfoManager.preloadCache()
|
||||||
|
|
||||||
// 开启网络监控
|
|
||||||
// NetworkManager.shared.networkStatusChanged = { status in
|
|
||||||
// print("🌍 网络状态更新:\(status)")
|
|
||||||
// }
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
// 🔍 DES加密已切换到OC版本
|
|
||||||
// print("🔐 使用OC版本的DES加密")
|
|
||||||
// DESEncryptOCTest.runInAppDelegate()
|
|
||||||
|
|
||||||
// 网络诊断 - 使用完整的登录参数测试
|
|
||||||
// let testURL = URL(string: "http://192.168.10.211:8080/oauth/token")!
|
|
||||||
// var request = URLRequest(url: testURL)
|
|
||||||
// request.httpMethod = "POST"
|
|
||||||
// request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
||||||
// request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
||||||
// request.setValue("zh-Hant", forHTTPHeaderField: "Accept-Language")
|
|
||||||
//
|
|
||||||
// // 添加完整的测试参数
|
|
||||||
// let testParameters: [String: Any] = [
|
|
||||||
// "ispType": "65535",
|
|
||||||
// "phone": "3+TbIQYiwIk=",
|
|
||||||
// "netType": 2,
|
|
||||||
// "channel": "molistar_enterprise",
|
|
||||||
// "version": "20.20.61",
|
|
||||||
// "pub_sign": "2E7C50AA17A20B32A0023F20B7ECE108",
|
|
||||||
// "osVersion": "16.4",
|
|
||||||
// "deviceId": "b715b75715e3417c9c70e72bbe502c6c",
|
|
||||||
// "grant_type": "password",
|
|
||||||
// "os": "iOS",
|
|
||||||
// "app": "youmi",
|
|
||||||
// "password": "nTW/lEgupIQ=",
|
|
||||||
// "client_id": "erban-client",
|
|
||||||
// "lang": "zh-Hant-CN",
|
|
||||||
// "client_secret": "uyzjdhds",
|
|
||||||
// "Accept-Language": "zh-Hant",
|
|
||||||
// "model": "iPhone XR",
|
|
||||||
// "appVersion": "1.0.0"
|
|
||||||
// ]
|
|
||||||
//
|
|
||||||
// do {
|
|
||||||
// let jsonData = try JSONSerialization.data(withJSONObject: testParameters, options: .prettyPrinted)
|
|
||||||
// request.httpBody = jsonData
|
|
||||||
//
|
|
||||||
// print("🛠 原生URLSession登录测试开始")
|
|
||||||
// print("📍 测试端点: \(testURL.absoluteString)")
|
|
||||||
// print("📦 请求参数: \(String(data: jsonData, encoding: .utf8) ?? "无法解析")")
|
|
||||||
//
|
|
||||||
// URLSession.shared.dataTask(with: request) { data, response, error in
|
|
||||||
// DispatchQueue.main.async {
|
|
||||||
// let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
|
|
||||||
// let responseString = data != nil ? String(data: data!, encoding: .utf8) ?? "无法解析响应" : "无数据"
|
|
||||||
//
|
|
||||||
// print("""
|
|
||||||
// === 网络诊断结果 ===
|
|
||||||
// 🔗 URL: \(testURL.absoluteString)
|
|
||||||
// 📊 响应状态码: \(statusCode)
|
|
||||||
// ❌ 错误信息: \(error?.localizedDescription ?? "无")
|
|
||||||
// 📦 原始数据: \(data?.count ?? 0) bytes
|
|
||||||
// 📄 响应内容: \(responseString)
|
|
||||||
// ==================
|
|
||||||
// """)
|
|
||||||
// }
|
|
||||||
// }.resume()
|
|
||||||
// } catch {
|
|
||||||
// print("❌ JSON序列化失败: \(error.localizedDescription)")
|
|
||||||
// }
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// NIMConfigurationManager.setupNimSDK()
|
// NIMConfigurationManager.setupNimSDK()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@@ -50,8 +50,6 @@ struct MainFeature {
|
|||||||
MeFeature()
|
MeFeature()
|
||||||
}
|
}
|
||||||
Reduce { state, action in
|
Reduce { state, action in
|
||||||
debugInfoSync("MainFeature action: \(action)")
|
|
||||||
debugInfoSync("MainFeature state: \(state)")
|
|
||||||
switch action {
|
switch action {
|
||||||
case .onAppear:
|
case .onAppear:
|
||||||
return .run { send in
|
return .run { send in
|
||||||
@@ -84,7 +82,6 @@ struct MainFeature {
|
|||||||
let nickname = userInfo?.nick ?? ""
|
let nickname = userInfo?.nick ?? ""
|
||||||
state.appSettingState = AppSettingFeature.State(nickname: nickname, avatarURL: avatarURL, userInfo: userInfo)
|
state.appSettingState = AppSettingFeature.State(nickname: nickname, avatarURL: avatarURL, userInfo: userInfo)
|
||||||
state.navigationPath.append(.appSetting)
|
state.navigationPath.append(.appSetting)
|
||||||
debugInfoSync("\(state.navigationPath)")
|
|
||||||
return .none
|
return .none
|
||||||
case .me:
|
case .me:
|
||||||
return .none
|
return .none
|
||||||
|
@@ -1,357 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
/// 数据迁移管理器
|
|
||||||
///
|
|
||||||
/// 负责将旧版本的 UserDefaults 存储数据迁移到新的 Keychain 存储方案。
|
|
||||||
/// 确保用户升级应用后无需重新登录。
|
|
||||||
///
|
|
||||||
/// 迁移策略:
|
|
||||||
/// 1. 检测旧数据是否存在
|
|
||||||
/// 2. 迁移到 Keychain
|
|
||||||
/// 3. 验证迁移结果
|
|
||||||
/// 4. 清理旧数据
|
|
||||||
@MainActor
|
|
||||||
final class DataMigrationManager {
|
|
||||||
|
|
||||||
// MARK: - 单例
|
|
||||||
static let shared = DataMigrationManager()
|
|
||||||
private init() {}
|
|
||||||
|
|
||||||
// MARK: - 迁移状态
|
|
||||||
private let migrationCompleteKey = "keychain_migration_completed_v1"
|
|
||||||
|
|
||||||
// MARK: - 旧版本存储键
|
|
||||||
private enum LegacyStorageKeys {
|
|
||||||
static let userId = "user_id"
|
|
||||||
static let accessToken = "access_token"
|
|
||||||
static let userInfo = "user_info"
|
|
||||||
static let accountModel = "account_model"
|
|
||||||
static let appLanguage = "AppLanguage"
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 迁移结果
|
|
||||||
enum MigrationResult {
|
|
||||||
case completed // 迁移完成
|
|
||||||
case alreadyMigrated // 已经迁移过
|
|
||||||
case noDataToMigrate // 没有需要迁移的数据
|
|
||||||
case failed(Error) // 迁移失败
|
|
||||||
|
|
||||||
var description: String {
|
|
||||||
switch self {
|
|
||||||
case .completed:
|
|
||||||
return "数据迁移完成"
|
|
||||||
case .alreadyMigrated:
|
|
||||||
return "数据已经迁移过"
|
|
||||||
case .noDataToMigrate:
|
|
||||||
return "没有需要迁移的数据"
|
|
||||||
case .failed(let error):
|
|
||||||
return "迁移失败: \(error.localizedDescription)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 公共方法
|
|
||||||
|
|
||||||
/// 执行数据迁移
|
|
||||||
/// - Returns: 迁移结果
|
|
||||||
func performMigration() -> MigrationResult {
|
|
||||||
debugInfoSync("🔄 开始检查数据迁移...")
|
|
||||||
|
|
||||||
// 检查是否已经迁移过
|
|
||||||
if isMigrationCompleted() {
|
|
||||||
debugInfoSync("✅ 数据已经迁移过,跳过迁移")
|
|
||||||
return .alreadyMigrated
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有需要迁移的数据
|
|
||||||
let legacyData = collectLegacyData()
|
|
||||||
if legacyData.isEmpty {
|
|
||||||
debugInfoSync("ℹ️ 没有发现需要迁移的数据")
|
|
||||||
markMigrationCompleted()
|
|
||||||
return .noDataToMigrate
|
|
||||||
}
|
|
||||||
|
|
||||||
debugInfoSync("📦 发现需要迁移的数据: \(legacyData.keys.joined(separator: ", "))")
|
|
||||||
|
|
||||||
do {
|
|
||||||
// 执行迁移
|
|
||||||
try migrateToKeychain(legacyData)
|
|
||||||
|
|
||||||
// 验证迁移结果
|
|
||||||
try verifyMigration(legacyData)
|
|
||||||
|
|
||||||
// 清理旧数据
|
|
||||||
cleanupLegacyData(legacyData.keys)
|
|
||||||
|
|
||||||
// 标记迁移完成
|
|
||||||
markMigrationCompleted()
|
|
||||||
|
|
||||||
debugInfoSync("✅ 数据迁移完成")
|
|
||||||
return .completed
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
debugErrorSync("❌ 数据迁移失败: \(error)")
|
|
||||||
return .failed(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 强制重新迁移(用于测试或修复)
|
|
||||||
func forceMigration() -> MigrationResult {
|
|
||||||
resetMigrationStatus()
|
|
||||||
return performMigration()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 私有方法
|
|
||||||
|
|
||||||
/// 检查迁移是否已完成
|
|
||||||
private func isMigrationCompleted() -> Bool {
|
|
||||||
return UserDefaults.standard.bool(forKey: migrationCompleteKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 标记迁移完成
|
|
||||||
private func markMigrationCompleted() {
|
|
||||||
UserDefaults.standard.set(true, forKey: migrationCompleteKey)
|
|
||||||
UserDefaults.standard.synchronize()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 重置迁移状态
|
|
||||||
private func resetMigrationStatus() {
|
|
||||||
UserDefaults.standard.removeObject(forKey: migrationCompleteKey)
|
|
||||||
UserDefaults.standard.synchronize()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 收集旧版本数据
|
|
||||||
private func collectLegacyData() -> [String: Any] {
|
|
||||||
let userDefaults = UserDefaults.standard
|
|
||||||
var legacyData: [String: Any] = [:]
|
|
||||||
|
|
||||||
// 检查各种旧数据
|
|
||||||
if let userId = userDefaults.string(forKey: LegacyStorageKeys.userId) {
|
|
||||||
legacyData[LegacyStorageKeys.userId] = userId
|
|
||||||
}
|
|
||||||
|
|
||||||
if let accessToken = userDefaults.string(forKey: LegacyStorageKeys.accessToken) {
|
|
||||||
legacyData[LegacyStorageKeys.accessToken] = accessToken
|
|
||||||
}
|
|
||||||
|
|
||||||
if let userInfoData = userDefaults.data(forKey: LegacyStorageKeys.userInfo) {
|
|
||||||
legacyData[LegacyStorageKeys.userInfo] = userInfoData
|
|
||||||
}
|
|
||||||
|
|
||||||
if let accountModelData = userDefaults.data(forKey: LegacyStorageKeys.accountModel) {
|
|
||||||
legacyData[LegacyStorageKeys.accountModel] = accountModelData
|
|
||||||
}
|
|
||||||
|
|
||||||
if let appLanguage = userDefaults.string(forKey: LegacyStorageKeys.appLanguage) {
|
|
||||||
legacyData[LegacyStorageKeys.appLanguage] = appLanguage
|
|
||||||
}
|
|
||||||
|
|
||||||
return legacyData
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 迁移数据到 Keychain
|
|
||||||
private func migrateToKeychain(_ legacyData: [String: Any]) throws {
|
|
||||||
let keychain = KeychainManager.shared
|
|
||||||
|
|
||||||
// 迁移 AccountModel(优先级最高,包含完整认证信息)
|
|
||||||
if let accountModelData = legacyData[LegacyStorageKeys.accountModel] as? Data {
|
|
||||||
do {
|
|
||||||
let accountModel = try JSONDecoder().decode(AccountModel.self, from: accountModelData)
|
|
||||||
try keychain.store(accountModel, forKey: "account_model")
|
|
||||||
debugInfoSync("✅ AccountModel 迁移成功")
|
|
||||||
} catch {
|
|
||||||
debugErrorSync("❌ AccountModel 迁移失败: \(error)")
|
|
||||||
// 如果 AccountModel 迁移失败,尝试从独立字段重建
|
|
||||||
try migrateAccountModelFromIndependentFields(legacyData)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果没有 AccountModel,从独立字段构建
|
|
||||||
try migrateAccountModelFromIndependentFields(legacyData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 迁移 UserInfo
|
|
||||||
if let userInfoData = legacyData[LegacyStorageKeys.userInfo] as? Data {
|
|
||||||
do {
|
|
||||||
let userInfo = try JSONDecoder().decode(UserInfo.self, from: userInfoData)
|
|
||||||
try keychain.store(userInfo, forKey: "user_info")
|
|
||||||
debugInfoSync("✅ UserInfo 迁移成功")
|
|
||||||
} catch {
|
|
||||||
debugErrorSync("❌ UserInfo 迁移失败: \(error)")
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 迁移语言设置
|
|
||||||
if let appLanguage = legacyData[LegacyStorageKeys.appLanguage] as? String {
|
|
||||||
try keychain.storeString(appLanguage, forKey: "AppLanguage")
|
|
||||||
debugInfoSync("✅ 语言设置迁移成功")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 从独立字段重建 AccountModel
|
|
||||||
private func migrateAccountModelFromIndependentFields(_ legacyData: [String: Any]) throws {
|
|
||||||
guard let userId = legacyData[LegacyStorageKeys.userId] as? String,
|
|
||||||
let accessToken = legacyData[LegacyStorageKeys.accessToken] as? String else {
|
|
||||||
debugInfoSync("ℹ️ 没有足够的独立字段来重建 AccountModel")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let accountModel = AccountModel(
|
|
||||||
uid: userId,
|
|
||||||
jti: nil,
|
|
||||||
tokenType: "bearer",
|
|
||||||
refreshToken: nil,
|
|
||||||
netEaseToken: nil,
|
|
||||||
accessToken: accessToken,
|
|
||||||
expiresIn: nil,
|
|
||||||
scope: nil,
|
|
||||||
ticket: nil
|
|
||||||
)
|
|
||||||
|
|
||||||
try KeychainManager.shared.store(accountModel, forKey: "account_model")
|
|
||||||
debugInfoSync("✅ 从独立字段重建 AccountModel 成功")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 验证迁移结果
|
|
||||||
private func verifyMigration(_ legacyData: [String: Any]) throws {
|
|
||||||
let keychain = KeychainManager.shared
|
|
||||||
|
|
||||||
// 验证 AccountModel
|
|
||||||
if legacyData[LegacyStorageKeys.accountModel] != nil ||
|
|
||||||
(legacyData[LegacyStorageKeys.userId] != nil && legacyData[LegacyStorageKeys.accessToken] != nil) {
|
|
||||||
let accountModel: AccountModel? = try keychain.retrieve(AccountModel.self, forKey: "account_model")
|
|
||||||
guard accountModel != nil else {
|
|
||||||
throw MigrationError.verificationFailed("AccountModel 验证失败")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证 UserInfo
|
|
||||||
if legacyData[LegacyStorageKeys.userInfo] != nil {
|
|
||||||
let userInfo: UserInfo? = try keychain.retrieve(UserInfo.self, forKey: "user_info")
|
|
||||||
guard userInfo != nil else {
|
|
||||||
throw MigrationError.verificationFailed("UserInfo 验证失败")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证语言设置
|
|
||||||
if legacyData[LegacyStorageKeys.appLanguage] != nil {
|
|
||||||
let appLanguage = try keychain.retrieveString(forKey: "AppLanguage")
|
|
||||||
guard appLanguage != nil else {
|
|
||||||
throw MigrationError.verificationFailed("语言设置验证失败")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debugInfoSync("✅ 迁移数据验证成功")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 清理旧数据
|
|
||||||
private func cleanupLegacyData(_ keys: Dictionary<String, Any>.Keys) {
|
|
||||||
let userDefaults = UserDefaults.standard
|
|
||||||
|
|
||||||
for key in keys {
|
|
||||||
userDefaults.removeObject(forKey: key)
|
|
||||||
debugInfoSync("🗑️ 清理旧数据: \(key)")
|
|
||||||
}
|
|
||||||
|
|
||||||
userDefaults.synchronize()
|
|
||||||
debugInfoSync("✅ 旧数据清理完成")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 迁移错误
|
|
||||||
|
|
||||||
enum MigrationError: Error, LocalizedError {
|
|
||||||
case verificationFailed(String)
|
|
||||||
case dataCorrupted(String)
|
|
||||||
case keychainError(Error)
|
|
||||||
|
|
||||||
var errorDescription: String? {
|
|
||||||
switch self {
|
|
||||||
case .verificationFailed(let message):
|
|
||||||
return "验证失败: \(message)"
|
|
||||||
case .dataCorrupted(let message):
|
|
||||||
return "数据损坏: \(message)"
|
|
||||||
case .keychainError(let error):
|
|
||||||
return "Keychain 错误: \(error.localizedDescription)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 应用启动时的迁移支持
|
|
||||||
|
|
||||||
extension DataMigrationManager {
|
|
||||||
|
|
||||||
/// 在应用启动时执行迁移
|
|
||||||
/// 这个方法应该在 AppDelegate 或 App 的初始化阶段调用
|
|
||||||
static func performStartupMigration() {
|
|
||||||
let migrationResult = DataMigrationManager.shared.performMigration()
|
|
||||||
|
|
||||||
switch migrationResult {
|
|
||||||
case .completed:
|
|
||||||
debugInfoSync("🎉 应用启动时数据迁移完成")
|
|
||||||
case .alreadyMigrated:
|
|
||||||
break // 静默处理
|
|
||||||
case .noDataToMigrate:
|
|
||||||
break // 静默处理
|
|
||||||
case .failed(let error):
|
|
||||||
debugErrorSync("⚠️ 应用启动时数据迁移失败: \(error)")
|
|
||||||
// 这里可以添加错误上报或降级策略
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 调试支持
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
extension DataMigrationManager {
|
|
||||||
|
|
||||||
/// 调试:打印旧数据信息
|
|
||||||
func debugPrintLegacyData() {
|
|
||||||
let legacyData = collectLegacyData()
|
|
||||||
debugInfoSync("🔍 旧版本数据:")
|
|
||||||
for (key, value) in legacyData {
|
|
||||||
debugInfoSync(" - \(key): \(type(of: value))")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 调试:模拟创建旧数据(用于测试)
|
|
||||||
func debugCreateLegacyData() {
|
|
||||||
let userDefaults = UserDefaults.standard
|
|
||||||
|
|
||||||
userDefaults.set("test_user_123", forKey: LegacyStorageKeys.userId)
|
|
||||||
userDefaults.set("test_access_token", forKey: LegacyStorageKeys.accessToken)
|
|
||||||
userDefaults.set("zh-Hans", forKey: LegacyStorageKeys.appLanguage)
|
|
||||||
userDefaults.synchronize()
|
|
||||||
|
|
||||||
debugInfoSync("🧪 已创建测试用的旧版本数据")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 调试:清除所有迁移相关数据
|
|
||||||
func debugClearAllData() {
|
|
||||||
// 清除 Keychain 数据
|
|
||||||
do {
|
|
||||||
try KeychainManager.shared.clearAll()
|
|
||||||
} catch {
|
|
||||||
debugErrorSync("❌ 清除 Keychain 数据失败: \(error)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除 UserDefaults 数据
|
|
||||||
let userDefaults = UserDefaults.standard
|
|
||||||
let allKeys = [
|
|
||||||
LegacyStorageKeys.userId,
|
|
||||||
LegacyStorageKeys.accessToken,
|
|
||||||
LegacyStorageKeys.userInfo,
|
|
||||||
LegacyStorageKeys.accountModel,
|
|
||||||
LegacyStorageKeys.appLanguage,
|
|
||||||
migrationCompleteKey
|
|
||||||
]
|
|
||||||
|
|
||||||
for key in allKeys {
|
|
||||||
userDefaults.removeObject(forKey: key)
|
|
||||||
}
|
|
||||||
userDefaults.synchronize()
|
|
||||||
|
|
||||||
debugInfoSync("🧪 已清除所有迁移相关数据")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
@@ -7,108 +7,105 @@ struct OptimizedDynamicCardView: View {
|
|||||||
let moment: MomentsInfo
|
let moment: MomentsInfo
|
||||||
let allMoments: [MomentsInfo]
|
let allMoments: [MomentsInfo]
|
||||||
let currentIndex: Int
|
let currentIndex: Int
|
||||||
|
// 新增:图片点击回调
|
||||||
|
let onImageTap: (_ images: [String], _ index: Int) -> Void
|
||||||
|
|
||||||
// 预览相关状态
|
init(moment: MomentsInfo, allMoments: [MomentsInfo], currentIndex: Int, onImageTap: @escaping (_ images: [String], _ index: Int) -> Void) {
|
||||||
@State private var showPreview = false
|
|
||||||
@State private var previewImageUrls: [String] = []
|
|
||||||
@State private var previewIndex: Int = 0
|
|
||||||
|
|
||||||
init(moment: MomentsInfo, allMoments: [MomentsInfo], currentIndex: Int) {
|
|
||||||
self.moment = moment
|
self.moment = moment
|
||||||
self.allMoments = allMoments
|
self.allMoments = allMoments
|
||||||
self.currentIndex = currentIndex
|
self.currentIndex = currentIndex
|
||||||
|
self.onImageTap = onImageTap
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
ZStack {
|
||||||
// 用户信息
|
// 背景层
|
||||||
HStack(alignment: .top) {
|
RoundedRectangle(cornerRadius: 12)
|
||||||
// 头像
|
.fill(Color.clear)
|
||||||
CachedAsyncImage(url: moment.avatar) { image in
|
.overlay(
|
||||||
image
|
RoundedRectangle(cornerRadius: 12)
|
||||||
.resizable()
|
.stroke(Color.white.opacity(0.1), lineWidth: 1)
|
||||||
.aspectRatio(contentMode: .fill)
|
)
|
||||||
} placeholder: {
|
.shadow(color: Color(red: 0.43, green: 0.43, blue: 0.43, opacity: 0.34), radius: 10.7, x: 0, y: 1.9)
|
||||||
Circle()
|
// 内容层
|
||||||
.fill(Color.gray.opacity(0.3))
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
.overlay(
|
// 用户信息
|
||||||
Text(String(moment.nick.prefix(1)))
|
HStack(alignment: .top) {
|
||||||
.font(.system(size: 16, weight: .medium))
|
// 头像
|
||||||
.foregroundColor(.white)
|
CachedAsyncImage(url: moment.avatar) { image in
|
||||||
)
|
image
|
||||||
}
|
.resizable()
|
||||||
.frame(width: 40, height: 40)
|
.aspectRatio(contentMode: .fill)
|
||||||
.clipShape(Circle())
|
} placeholder: {
|
||||||
|
Circle()
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
.fill(Color.gray.opacity(0.3))
|
||||||
Text(moment.nick)
|
.overlay(
|
||||||
.font(.system(size: 16, weight: .medium))
|
Text(String(moment.nick.prefix(1)))
|
||||||
.foregroundColor(.white)
|
.font(.system(size: 16, weight: .medium))
|
||||||
Text("ID: \(moment.uid)")
|
.foregroundColor(.white)
|
||||||
.font(.system(size: 12))
|
)
|
||||||
.foregroundColor(.white.opacity(0.6))
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
// 时间(原VIP位置)
|
|
||||||
Text(formatDisplayTime(moment.publishTime))
|
|
||||||
.font(.system(size: 12, weight: .bold))
|
|
||||||
.foregroundColor(.white.opacity(0.8))
|
|
||||||
.padding(.horizontal, 6)
|
|
||||||
.padding(.vertical, 2)
|
|
||||||
.background(Color.white.opacity(0.15))
|
|
||||||
.cornerRadius(4)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 动态内容
|
|
||||||
if !moment.content.isEmpty {
|
|
||||||
Text(moment.content)
|
|
||||||
.font(.system(size: 14))
|
|
||||||
.foregroundColor(.white.opacity(0.9))
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.padding(.leading, 40 + 8) // 与用户名左边对齐
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优化的图片网格
|
|
||||||
if let images = moment.dynamicResList, !images.isEmpty {
|
|
||||||
OptimizedImageGrid(images: images) { tappedIndex in
|
|
||||||
previewImageUrls = images.map { $0.resUrl ?? "" }
|
|
||||||
previewIndex = tappedIndex
|
|
||||||
showPreview = true
|
|
||||||
}
|
|
||||||
.padding(.bottom, images.count == 2 ? 16 : 0) // 两张图片时增加底部间距
|
|
||||||
}
|
|
||||||
|
|
||||||
// 互动按钮
|
|
||||||
HStack(spacing: 20) {
|
|
||||||
// Like 按钮左对齐
|
|
||||||
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))
|
.frame(width: 40, height: 40)
|
||||||
|
.clipShape(Circle())
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(moment.nick)
|
||||||
|
.font(.system(size: 16, weight: .medium))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
Text("ID: \(moment.uid)")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundColor(.white.opacity(0.6))
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
// 时间(原VIP位置)
|
||||||
|
Text(formatDisplayTime(moment.publishTime))
|
||||||
|
.font(.system(size: 12, weight: .bold))
|
||||||
|
.foregroundColor(.white.opacity(0.8))
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.background(Color.white.opacity(0.15))
|
||||||
|
.cornerRadius(4)
|
||||||
}
|
}
|
||||||
Spacer()
|
|
||||||
|
// 动态内容
|
||||||
|
if !moment.content.isEmpty {
|
||||||
|
Text(moment.content)
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.foregroundColor(.white.opacity(0.9))
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.padding(.leading, 40 + 8) // 与用户名左边对齐
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优化的图片网格
|
||||||
|
if let images = moment.dynamicResList, !images.isEmpty {
|
||||||
|
OptimizedImageGrid(images: images) { tappedIndex in
|
||||||
|
let urls = images.map { $0.resUrl ?? "" }
|
||||||
|
onImageTap(urls, tappedIndex)
|
||||||
|
}
|
||||||
|
.padding(.bottom, images.count == 2 ? 46 : 0) // 两张图片时增加底部间距
|
||||||
|
}
|
||||||
|
|
||||||
|
// 互动按钮
|
||||||
|
HStack(spacing: 20) {
|
||||||
|
// Like 按钮左对齐
|
||||||
|
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(.top, 8)
|
.padding(16)
|
||||||
}
|
}
|
||||||
.padding(16)
|
|
||||||
.background(
|
|
||||||
Color.white.opacity(0.1)
|
|
||||||
.cornerRadius(12)
|
|
||||||
)
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
preloadNearbyImages()
|
preloadNearbyImages()
|
||||||
}
|
}
|
||||||
// 图片预览弹窗
|
|
||||||
.fullScreenCover(isPresented: $showPreview) {
|
|
||||||
ImagePreviewPager(images: previewImageUrls, currentIndex: $previewIndex) {
|
|
||||||
showPreview = false
|
|
||||||
previewImageUrls = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func formatTime(_ timestamp: Int) -> String {
|
private func formatTime(_ timestamp: Int) -> String {
|
||||||
|
7
yana/Views/Components/PreviewItem.swift
Normal file
7
yana/Views/Components/PreviewItem.swift
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct PreviewItem: Identifiable, Equatable {
|
||||||
|
let id = UUID()
|
||||||
|
let images: [String]
|
||||||
|
let index: Int
|
||||||
|
}
|
@@ -1,9 +1,10 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ComposableArchitecture
|
import ComposableArchitecture
|
||||||
//import OptimizedDynamicCardView // 导入新组件
|
|
||||||
|
|
||||||
struct FeedListView: View {
|
struct FeedListView: View {
|
||||||
let store: StoreOf<FeedListFeature>
|
let store: StoreOf<FeedListFeature>
|
||||||
|
// 新增:图片预览状态
|
||||||
|
@State private var previewItem: PreviewItem? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||||
@@ -19,43 +20,35 @@ struct FeedListView: View {
|
|||||||
.ignoresSafeArea(.all)
|
.ignoresSafeArea(.all)
|
||||||
VStack(alignment: .center, spacing: 0) {
|
VStack(alignment: .center, spacing: 0) {
|
||||||
// 顶部栏
|
// 顶部栏
|
||||||
HStack {
|
ZStack {
|
||||||
Button(action: {
|
HStack {
|
||||||
viewStore.send(.testButtonTapped)
|
Spacer(minLength: 0)
|
||||||
}) {
|
Text(NSLocalizedString("feedList.title", comment: "Enjoy your Life Time"))
|
||||||
Text("测试")
|
.font(.system(size: 22, weight: .semibold))
|
||||||
.font(.system(size: 14))
|
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding(.horizontal, 12)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
.padding(.vertical, 6)
|
Spacer(minLength: 0)
|
||||||
.background(Color.blue.opacity(0.7))
|
|
||||||
.cornerRadius(8)
|
|
||||||
}
|
}
|
||||||
Spacer(minLength: 0)
|
HStack {
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
Text(NSLocalizedString("feedList.title", comment: "Enjoy your Life Time"))
|
Button(action: {
|
||||||
.font(.system(size: 22, weight: .semibold))
|
viewStore.send(.editFeedButtonTapped)
|
||||||
.foregroundColor(.white)
|
}) {
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
Image("add icon")
|
||||||
Spacer(minLength: 0)
|
.resizable()
|
||||||
Button(action: {
|
.frame(width: 40, height: 40)
|
||||||
viewStore.send(.editFeedButtonTapped)
|
}
|
||||||
}) {
|
|
||||||
Image("add icon")
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 36, height: 36)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
.padding(.top, geometry.safeAreaInsets.top)
|
|
||||||
// 其他内容
|
// 其他内容
|
||||||
Image(systemName: "heart.fill")
|
Image("Volume")
|
||||||
.font(.system(size: 60))
|
.frame(width: 56, height: 41)
|
||||||
.foregroundColor(.red)
|
.padding(.top, 16)
|
||||||
.padding(.top, 40)
|
|
||||||
Text(NSLocalizedString("feedList.slogan", comment: "The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable."))
|
Text(NSLocalizedString("feedList.slogan", comment: "The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable."))
|
||||||
.font(.system(size: 16))
|
.font(.system(size: 16))
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.leading)
|
||||||
.foregroundColor(.white.opacity(0.9))
|
.foregroundColor(.white.opacity(0.9))
|
||||||
.padding(.horizontal, 30)
|
.padding(.horizontal, 30)
|
||||||
.padding(.bottom, 30)
|
.padding(.bottom, 30)
|
||||||
@@ -81,7 +74,14 @@ struct FeedListView: View {
|
|||||||
WithPerceptionTracking {
|
WithPerceptionTracking {
|
||||||
LazyVStack(spacing: 16) {
|
LazyVStack(spacing: 16) {
|
||||||
ForEach(Array(viewStore.moments.enumerated()), id: \ .element.dynamicId) { index, moment in
|
ForEach(Array(viewStore.moments.enumerated()), id: \ .element.dynamicId) { index, moment in
|
||||||
OptimizedDynamicCardView(moment: moment, allMoments: viewStore.moments, currentIndex: index)
|
OptimizedDynamicCardView(
|
||||||
|
moment: moment,
|
||||||
|
allMoments: viewStore.moments,
|
||||||
|
currentIndex: index,
|
||||||
|
onImageTap: { images, tappedIndex in
|
||||||
|
previewItem = PreviewItem(images: images, index: tappedIndex)
|
||||||
|
}
|
||||||
|
)
|
||||||
// 上拉加载更多触发点
|
// 上拉加载更多触发点
|
||||||
if index == viewStore.moments.count - 1, viewStore.hasMore, !viewStore.isLoadingMore {
|
if index == viewStore.moments.count - 1, viewStore.hasMore, !viewStore.isLoadingMore {
|
||||||
Color.clear
|
Color.clear
|
||||||
@@ -97,6 +97,8 @@ struct FeedListView: View {
|
|||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
|
// 新增底部间距
|
||||||
|
Color.clear.frame(height: 120)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.top, 10)
|
.padding(.top, 10)
|
||||||
@@ -133,6 +135,12 @@ struct FeedListView: View {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// 新增:图片预览弹窗
|
||||||
|
.fullScreenCover(item: $previewItem) { item in
|
||||||
|
ImagePreviewPager(images: item.images, currentIndex: .constant(item.index)) {
|
||||||
|
previewItem = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,8 @@ import ComposableArchitecture
|
|||||||
|
|
||||||
struct MeView: View {
|
struct MeView: View {
|
||||||
let store: StoreOf<MeFeature>
|
let store: StoreOf<MeFeature>
|
||||||
|
// 新增:图片预览状态
|
||||||
|
@State private var previewItem: PreviewItem? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
WithPerceptionTracking {
|
WithPerceptionTracking {
|
||||||
@@ -14,8 +16,8 @@ struct MeView: View {
|
|||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.clipped()
|
.clipped()
|
||||||
.ignoresSafeArea(.all)
|
.ignoresSafeArea(.all)
|
||||||
VStack(spacing: 0) {
|
// 顶部栏,右上角设置按钮
|
||||||
// 顶部栏,右上角设置按钮
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||||
@@ -23,13 +25,16 @@ struct MeView: View {
|
|||||||
viewStore.send(.settingButtonTapped)
|
viewStore.send(.settingButtonTapped)
|
||||||
}) {
|
}) {
|
||||||
Image(systemName: "gearshape")
|
Image(systemName: "gearshape")
|
||||||
.font(.system(size: 22, weight: .medium))
|
.font(.system(size: 33, weight: .medium))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
}
|
}
|
||||||
.padding(.trailing, 16)
|
.padding(.trailing, 16)
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
VStack(spacing: 16) {
|
||||||
// 用户信息区域
|
// 用户信息区域
|
||||||
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
WithViewStore(self.store, observe: { $0 }) { viewStore in
|
||||||
if viewStore.isLoadingUserInfo {
|
if viewStore.isLoadingUserInfo {
|
||||||
@@ -61,6 +66,7 @@ struct MeView: View {
|
|||||||
.font(.system(size: 14))
|
.font(.system(size: 14))
|
||||||
.foregroundColor(.white.opacity(0.7))
|
.foregroundColor(.white.opacity(0.7))
|
||||||
}
|
}
|
||||||
|
.padding(.top, 0)
|
||||||
.frame(height: 130)
|
.frame(height: 130)
|
||||||
} else {
|
} else {
|
||||||
Spacer().frame(height: 130)
|
Spacer().frame(height: 130)
|
||||||
@@ -96,9 +102,17 @@ struct MeView: View {
|
|||||||
ScrollView {
|
ScrollView {
|
||||||
WithPerceptionTracking {
|
WithPerceptionTracking {
|
||||||
LazyVStack(spacing: 12) {
|
LazyVStack(spacing: 12) {
|
||||||
ForEach(Array(viewStore.moments.enumerated()), id: \.element.dynamicId) { index, moment in
|
ForEach(viewStore.moments.indices, id: \ .self) { index in
|
||||||
OptimizedDynamicCardView(moment: moment, allMoments: viewStore.moments, currentIndex: index)
|
let moment = viewStore.moments[index]
|
||||||
.padding(.horizontal, 12)
|
OptimizedDynamicCardView(
|
||||||
|
moment: moment,
|
||||||
|
allMoments: viewStore.moments,
|
||||||
|
currentIndex: index,
|
||||||
|
onImageTap: { images, tappedIndex in
|
||||||
|
previewItem = PreviewItem(images: images, index: tappedIndex)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
}
|
}
|
||||||
if viewStore.hasMore {
|
if viewStore.hasMore {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
@@ -106,6 +120,8 @@ struct MeView: View {
|
|||||||
viewStore.send(.loadMore)
|
viewStore.send(.loadMore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 新增底部间距
|
||||||
|
Color.clear.frame(height: 120)
|
||||||
}
|
}
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
}
|
}
|
||||||
@@ -118,11 +134,18 @@ struct MeView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, alignment: .top)
|
||||||
|
.padding(.top, 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
ViewStore(self.store, observe: { $0 }).send(.onAppear)
|
ViewStore(self.store, observe: { $0 }).send(.onAppear)
|
||||||
}
|
}
|
||||||
|
// 新增:图片预览弹窗
|
||||||
|
.fullScreenCover(item: $previewItem) { item in
|
||||||
|
ImagePreviewPager(images: item.images, currentIndex: .constant(item.index)) {
|
||||||
|
previewItem = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user