From 8b09653c4c71c9949a9e0570af427838e9463b1f Mon Sep 17 00:00:00 2001 From: edwinQQQ Date: Wed, 23 Jul 2025 19:17:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=96=B0=E5=A2=9E=E6=88=91=E7=9A=84?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E8=A7=86=E5=9B=BE=E5=8F=8A=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在HomeFeature中添加MeDynamicFeature以管理用户动态状态。 - 在MainFeature中集成MeDynamicFeature,支持动态内容的加载与展示。 - 新增MeDynamicView以展示用户的动态列表,支持下拉刷新和上拉加载更多功能。 - 更新MeView以集成用户动态视图,提升用户体验。 - 在APIEndpoints中新增getMyDynamic端点以支持获取用户动态信息。 - 更新DynamicsModels以适应新的动态数据结构,确保数据解析的准确性。 - 在OptimizedDynamicCardView中优化图片处理逻辑,提升动态展示效果。 - 更新相关视图组件以支持动态内容的展示与交互,增强用户体验。 --- .cursor/rules/swift-assistant-style.mdc | 41 +- yana.xcodeproj/project.pbxproj | 8 +- yana/APIs/APIEndpoints.swift | 1 + yana/APIs/DynamicsModels.swift | 83 +++- yana/APIs/LoginModels.swift | 206 ++++++++- yana/APIs/data.md | 422 ++++++++++++++---- yana/Features/HomeFeature.swift | 163 +++---- yana/Features/MainFeature.swift | 26 ++ yana/Features/MeDynamicFeature.swift | 91 ++++ .../Components/OptimizedDynamicCardView.swift | 6 +- yana/Views/HomeView.swift | 11 +- yana/Views/MainView.swift | 16 +- yana/Views/MeDynamicView.swift | 75 ++++ yana/Views/MeView.swift | 297 +++++++----- 项目问题排查与解决流程.md | 10 +- 15 files changed, 1097 insertions(+), 359 deletions(-) create mode 100644 yana/Features/MeDynamicFeature.swift create mode 100644 yana/Views/MeDynamicView.swift diff --git a/.cursor/rules/swift-assistant-style.mdc b/.cursor/rules/swift-assistant-style.mdc index dfc3b9e..90ed7fd 100644 --- a/.cursor/rules/swift-assistant-style.mdc +++ b/.cursor/rules/swift-assistant-style.mdc @@ -4,33 +4,38 @@ globs: alwaysApply: true --- # CONTEXT - I wish to receive advice using the latest tools and seek step-by-step guidance to fully understand the implementation process. - # OBJECTIVE + I wish to receive advice using the latest tools and seek step-by-step guidance to fully understand the implementation process. + +## OBJECTIVE + As an expert AI programming assistant, your task is to provide me with clear and readable SwiftUI code. You should: - - Utilize the latest versions of SwiftUI and Swift, being familiar with the newest features and best practices. - - Provide careful and accurate answers that are well-founded and thoughtfully considered. - - **Explicitly use the Chain-of-Thought (CoT) method in your reasoning and answers, explaining your thought process step by step.** - - Strictly adhere to my requirements and meticulously complete the tasks. - - Begin by outlining your proposed approach with detailed steps or pseudocode. - - Upon confirming the plan, proceed to write the code. - # STYLE - - Keep answers concise and direct, minimizing unnecessary wording. - - Emphasize code readability over performance optimization. - - Maintain a professional and supportive tone, ensuring clarity of content. +- Utilize the latest versions of SwiftUI and Swift, being familiar with the newest features and best practices. +- Provide careful and accurate answers that are well-founded and thoughtfully considered. +- **Explicitly use the Chain-of-Thought (CoT) method in your reasoning and answers, explaining your thought process step by step.** +- Strictly adhere to my requirements and meticulously complete the tasks. +- Begin by outlining your proposed approach with detailed steps or pseudocode. +- Upon confirming the plan, proceed to write the code. + +## STYLE + +- Keep answers concise and direct, minimizing unnecessary wording. +- Emphasize code readability over performance optimization. +- Maintain a professional and supportive tone, ensuring clarity of content. - - # AUDIENCE +## AUDIENCE + The target audience is me, a native Chinese developer eager to learn Swift 6 and Xcode 15.9, seeking guidance and advice on utilizing the latest technologies. - # RESPONSE FORMAT - - **Utilize the Chain-of-Thought (CoT) method to reason and respond, explaining your thought process step by step.** - - The reply should include: +## RESPONSE FORMAT + +- **Utilize the Chain-of-Thought (CoT) method to reason and respond, explaining your thought process step by step.** +- The reply should include: 1. **Step-by-Step Plan**: Describe the implementation process with detailed pseudocode or step-by-step explanations, showcasing your thought process. 2. **Code Implementation**: Provide correct, up-to-date, error-free, fully functional, runnable, secure, and efficient code. The code should: - Include all necessary imports and properly name key components. - Fully implement all requested features, leaving no to-dos, placeholders, or omissions. 3. **Concise Response**: Minimize unnecessary verbosity, focusing only on essential information. - - If a correct answer may not exist, please point it out. If you do not know the answer, please honestly inform me rather than guessing. +- If a correct answer may not exist, please point it out. If you do not know the answer, please honestly inform me rather than guessing. diff --git a/yana.xcodeproj/project.pbxproj b/yana.xcodeproj/project.pbxproj index 08ee6d1..172b983 100644 --- a/yana.xcodeproj/project.pbxproj +++ b/yana.xcodeproj/project.pbxproj @@ -583,14 +583,16 @@ isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = EKM7RAGNA6; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yanaAPITests; + PRODUCT_BUNDLE_IDENTIFIER = com.junpeiqi.eparty; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -610,7 +612,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = EKM7RAGNA6; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yanaAPITests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/yana/APIs/APIEndpoints.swift b/yana/APIs/APIEndpoints.swift index 583b149..ee911c1 100644 --- a/yana/APIs/APIEndpoints.swift +++ b/yana/APIs/APIEndpoints.swift @@ -23,6 +23,7 @@ enum APIEndpoint: String, CaseIterable { case tcToken = "/tencent/cos/getToken" // 新增:腾讯云 COS Token 获取端点 case publishFeed = "/dynamic/square/publish" // 发布动态 case getUserInfo = "/user/get" // 新增:获取用户信息端点 + case getMyDynamic = "/dynamic/getMyDynamic" // Web 页面路径 case userAgreement = "/modules/rule/protocol.html" diff --git a/yana/APIs/DynamicsModels.swift b/yana/APIs/DynamicsModels.swift index 84652da..1d5dfee 100644 --- a/yana/APIs/DynamicsModels.swift +++ b/yana/APIs/DynamicsModels.swift @@ -23,7 +23,6 @@ public struct MomentsInfo: Codable, Equatable, Sendable { let uid: Int let nick: String let avatar: String - let gender: Int let type: Int let content: String let likeCount: Int @@ -31,35 +30,30 @@ public struct MomentsInfo: Codable, Equatable, Sendable { let commentCount: Int let publishTime: Int let worldId: Int - let squareTop: Int - let topicTop: Int - let newUser: Bool - let defUser: Int let status: Int - let scene: String + // data.md 里部分字段可选 + let playCount: Int? let dynamicResList: [MomentsPicture]? + // 以下字段后端未返回,全部可选 + let gender: Int? + let squareTop: Int? + let topicTop: Int? + let newUser: Bool? + let defUser: Int? + let scene: String? let userVipInfoVO: UserVipInfo? - - // 头饰相关 - 全部可选 let headwearPic: String? let headwearEffect: String? let headwearType: Int? let headwearName: String? let headwearId: Int? - - // 等级相关 - 全部可选 let experLevelPic: String? let charmLevelPic: String? - - // 其他可选字段 let isCustomWord: Bool? let labelList: [String]? - - // 计算属性:将Int转换为Bool - var isSquareTop: Bool { squareTop != 0 } - var isTopicTop: Bool { topicTop != 0 } - - // 计算属性:格式化时间戳 + // 计算属性 + var isSquareTop: Bool { (squareTop ?? 0) != 0 } + var isTopicTop: Bool { (topicTop ?? 0) != 0 } var formattedPublishTime: Date { Date(timeIntervalSince1970: TimeInterval(publishTime) / 1000.0) } @@ -67,11 +61,11 @@ public struct MomentsInfo: Codable, Equatable, Sendable { /// 动态图片信息 struct MomentsPicture: Codable, Equatable, Sendable { - let id: Int - let resUrl: String - let format: String - let width: Int - let height: Int + let id: Int? + let resUrl: String? + let format: String? + let width: Int? + let height: Int? let resDuration: Int? // 可选字段,因为有些图片没有这个字段 } @@ -244,3 +238,46 @@ struct PublishFeedResponse: Codable, Equatable { struct PublishFeedData: Codable, Equatable { let dynamicId: Int? } + +// MARK: - 我的动态 API 请求 + +/// 我的动态响应结构 +struct MyMomentsResponse: Codable, Equatable, Sendable { + let code: Int + let message: String + let data: [MomentsInfo]? + let timestamp: Int? +} + +struct GetMyDynamicRequest: APIRequestProtocol { + typealias Response = MyMomentsResponse + + let endpoint: String = APIEndpoint.getMyDynamic.path + let method: HTTPMethod = .POST + + let fromUid: Int + let uid: Int + let page: Int + let pageSize: Int + + init(fromUid: Int, uid: Int, page: Int = 1, pageSize: Int = 20) { + self.fromUid = fromUid + self.uid = uid + self.page = page + self.pageSize = pageSize + } + + var queryParameters: [String: String]? { + [ + "fromUid": String(fromUid), + "uid": String(uid), + "page": String(page), + "pageSize": String(pageSize) + ] + } + + var bodyParameters: [String: Any]? { nil } + var includeBaseParameters: Bool { true } + var shouldShowLoading: Bool { true } + var shouldShowError: Bool { true } +} diff --git a/yana/APIs/LoginModels.swift b/yana/APIs/LoginModels.swift index 47f3017..38421cf 100644 --- a/yana/APIs/LoginModels.swift +++ b/yana/APIs/LoginModels.swift @@ -146,29 +146,209 @@ struct IDLoginData: Codable, Equatable { // MARK: - User Info Model struct UserInfo: Codable, Equatable { - let userId: String? - let username: String? - let nickname: String? + let uid: Int? + let userId: String? // 兼容旧字段 + let nick: String? + let nickname: String? // 兼容旧字段 let avatar: String? - let email: String? - let phone: String? - let status: String? - let createTime: String? - let updateTime: String? - + let region: String? + let regionDesc: String? + let gender: Int? + let birth: Int64? + let userDesc: String? + let userLevelVo: UserLevelVo? + let userVipInfoVO: UserVipInfoVO? + let medalsPic: [MedalsPic]? + let userHeadwear: UserHeadwear? + let privatePhoto: [PrivatePhoto]? + let createTime: Int64? + let phoneAreaCode: String? + let erbanNo: Int? + let isCertified: Bool? + let isBindPhone: Bool? + let isBindApple: Bool? + let isBindPasswd: Bool? + let isBindPaymentPwd: Bool? + let banAccount: Bool? + let visitNum: Int? + let fansNum: Int? + let followNum: Int? + let visitHide: Bool? + let visitListView: Bool? + let newUser: Bool? + let defUser: Int? + let platformRole: Int? + let bindType: Int? + let showLimitCharge: Bool? + let uploadGifAvatarPrice: Int? + let hasRegPacket: Bool? + let hasPrettyErbanNo: Bool? + let hasSuperRole: Bool? + let isRechargeUser: Bool? + let isFirstCharge: Bool? + let fromSayHelloChannel: Bool? + let partitionId: Int? + let useStatus: Int? + let micNickColor: String? + let micCircle: String? + let audioCard: AudioCard? + let userInfoSkillVo: UserInfoSkillVo? + let userInfoCardPic: String? + let iosBubbleUrl: String? + let androidBubbleUrl: String? + let status: String? // 兼容旧字段 + let username: String? // 兼容旧字段 + let email: String? // 兼容旧字段 + let phone: String? // 兼容旧字段 + let updateTime: String? // 兼容旧字段 + enum CodingKeys: String, CodingKey { + case uid case userId = "user_id" - case username + case nick case nickname case avatar + case region + case regionDesc + case gender + case birth + case userDesc + case userLevelVo + case userVipInfoVO + case medalsPic + case userHeadwear + case privatePhoto + case createTime + case phoneAreaCode + case erbanNo + case isCertified + case isBindPhone + case isBindApple + case isBindPasswd + case isBindPaymentPwd + case banAccount + case visitNum + case fansNum + case followNum + case visitHide + case visitListView + case newUser + case defUser + case platformRole + case bindType + case showLimitCharge + case uploadGifAvatarPrice + case hasRegPacket + case hasPrettyErbanNo + case hasSuperRole + case isRechargeUser + case isFirstCharge + case fromSayHelloChannel + case partitionId + case useStatus + case micNickColor + case micCircle + case audioCard + case userInfoSkillVo + case userInfoCardPic + case iosBubbleUrl + case androidBubbleUrl + case status + case username case email case phone - case status - case createTime = "create_time" - case updateTime = "update_time" + case updateTime } } +// MARK: - 嵌套对象结构体 +struct UserLevelVo: Codable, Equatable { + let experUrl: String? + let charmLevelSeq: Int? + let experLevelName: String? + let charmLevelName: String? + let charmAmount: Int? + let experLevelGrp: String? + let charmUrl: String? + let experLevelSeq: Int? + let experAmount: Int? + let charmLevelGrp: String? +} + +struct UserVipInfoVO: Codable, Equatable { + let vipIcon: String? + let nameplateId: Int? + let vipLogo: String? + let userCardBG: String? + let preventKick: Bool? + let preventTrace: Bool? + let preventFollow: Bool? + let micNickColour: String? + let micCircle: String? + let enterRoomEffects: String? + let medalSeat: Int? + let friendNickColour: String? + let visitHide: Bool? + let visitListView: Bool? + let privateChatLimit: Bool? + let nameplateUrl: String? + let roomPicScreen: Bool? + let uploadGifAvatar: Bool? + let expireTime: Int64? + let enterHide: Bool? + let vipLevel: Int? + let vipName: String? +} + +struct MedalsPic: Codable, Equatable { + let picUrl: String? + let mp4Url: String? +} + +struct UserHeadwear: Codable, Equatable { + let expireTime: Int64? + let renewPrice: Int? + let uid: Int? + let comeFrom: Int? + let labelType: Int? + let limitDesc: String? + let redirectLink: String? + let headwearId: Int? + let buyTime: Int64? + let pic: String? + let used: Bool? + let price: Int? + let originalPrice: Int? + let type: Int? + let days: Int? + let headwearName: String? + let effect: String? + let expireDays: Int? + let status: Int? +} + +struct PrivatePhoto: Codable, Equatable { + let seqNo: Int? + let photoUrl: String? + let createTime: Int64? + let review: Bool? + let pid: Int? +} + +struct AudioCard: Codable, Equatable { + let uid: Int? + let status: Int? +} + +struct UserInfoSkillVo: Codable, Equatable { + let liveTag: Bool? + let liveSkillVoList: [LiveSkillVo]? +} + +struct LiveSkillVo: Codable, Equatable { + // 具体字段根据API返回补充,这里暂留空 +} + // MARK: - Login Helper struct LoginHelper { diff --git a/yana/APIs/data.md b/yana/APIs/data.md index 83d449f..0b03d25 100644 --- a/yana/APIs/data.md +++ b/yana/APIs/data.md @@ -1,92 +1,330 @@ -## 📝 给继任者的详细工作交接说明 - -亲爱的继任者,我刚刚为这个 **yana iOS 项目**完成了一个完整的图片缓存优化工作。以下是关键信息: - -### 🎯 已完成的核心工作 - -1. **解决了重大性能问题**: - - **问题**:FeedView 中图片每次滚动都重新加载,用户体验极差 - - **原因**:AsyncImage 缓存不足,没有预加载机制,cell 重用时图片丢失 - -2. **创建了企业级图片缓存系统**: - - **文件**:`yana/Utils/ImageCacheManager.swift` - - **功能**:三层缓存(内存+磁盘+网络)+ 智能预加载 + 任务去重 - -3. **优化了 FeedView 架构**: - - **文件**:`yana/Views/FeedView.swift` - - **改进**:使用 `CachedAsyncImage` 替代 `AsyncImage`,添加预加载机制 - -### ✅ 技术架构详情 - -#### **ImageCacheManager 核心特性**: -- **内存缓存**:NSCache,50MB 限制,100张图片 -- **磁盘缓存**:Documents/ImageCache,100MB 限制,SHA256 文件名 -- **预加载**:当前位置前后2个动态的所有图片 -- **任务去重**:同一图片多次请求共享下载任务 - -#### **CachedAsyncImage 组件**: -- **缓存优先级**:内存 → 磁盘 → 网络 -- **异步加载**:不阻塞主线程 -- **SwiftUI 兼容**:完全兼容现有 AsyncImage 语法 - -#### **FeedView 优化**: -- **OptimizedDynamicCardView**:使用缓存图片组件 -- **OptimizedImageGrid**:优化的图片网格 -- **智能预加载**:onAppear 时触发相邻内容预加载 - -### 🔧 重要的技术细节 - -1. **哈希冲突解决**: - - 项目中已有 `String+MD5.swift` 文件 - - 使用现有的 `sha256()` 和 `md5()` 方法,避免重复声明 - -2. **兼容性处理**: - - iOS 13+:使用 CryptoKit 的 SHA256 - - iOS 13以下:使用 CommonCrypto 的 MD5 - -3. **Bridging Header 配置**: - - 已添加 `#import ` - -### 🚀 性能提升效果 - -| 优化前 | 优化后 | -|--------|--------| -| ❌ 每次滚动重新加载图片 | ✅ 缓存图片瞬间显示 | -| ❌ 频繁网络请求 | ✅ 大幅减少网络请求 | -| ❌ 用户体验差 | ✅ 流畅滚动体验 | - -### 📋 项目上下文回顾 - -1. **API 功能已完成**: - - 动态内容 API 集成完毕(DynamicsModels.swift + FeedFeature.swift) - - 数据解析问题已解决(类型匹配修复) - - TCA 架构状态管理正常工作 - -2. **当前状态**: - - ✅ 编译成功 - - ✅ API 数据正常显示 - - ✅ 图片缓存系统就绪 - - ✅ 性能优化完成 - -### 🔍 可能的后续工作 - -用户可能需要: -1. **功能扩展**:点赞、评论、分享等交互功能 -2. **UI 优化**:更丰富的动画效果、主题切换 -3. **性能监控**:添加缓存命中率统计、内存使用监控 -4. **错误处理**:网络异常时的重试机制优化 - -### 💡 重要提醒 - -- **用户是中文开发者**:需要中文回复,使用 Chain-of-Thought 思考过程 -- **项目基于 iOS 15.6**:注意兼容性要求 -- **TCA 架构**:遵循项目现有的 TCA 模式 -- **图片缓存系统**:已经是生产就绪的企业级方案,无需重构 - -### 🎉 工作成果 - -这次优化彻底解决了图片重复加载的性能问题,用户现在可以享受流畅的滚动体验。缓存系统设计完善,支持大规模图片内容,为后续功能扩展奠定了坚实基础。 - -**继任者,你接手的是一个功能完整、性能优秀的动态内容展示系统!** 🚀 - -祝你工作顺利! \ No newline at end of file +📦 Response Data: +{ + "code" : 200, + "message" : "success", + "data" : [ + { + "isLike" : false, + "uid" : 3184, + "playCount" : 0, + "worldId" : -1, + "likeCount" : 0, + "type" : 0, + "dynamicId" : 267, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1753182147000, + "status" : 0, + "content" : "我" + }, + { + "isLike" : false, + "uid" : 3184, + "playCount" : 0, + "dynamicResList" : [ + { + "height" : 3024, + "id" : 443, + "width" : 4032, + "resUrl" : "https:\/\/image.molistar.xyz\/images\/C32EB0F8-CBF5-4F4B-8114-C3C7E1AF192F.jpg", + "format" : "jpeg" + } + ], + "worldId" : -1, + "likeCount" : 0, + "type" : 2, + "dynamicId" : 266, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1753181890000, + "status" : 0, + "content" : "" + }, + { + "isLike" : false, + "uid" : 3184, + "playCount" : 0, + "dynamicResList" : [ + { + "height" : 828, + "id" : 442, + "width" : 828, + "resUrl" : "https:\/\/image.molistar.xyz\/images\/1E8FE811-1989-4337-BDEE-63554F92A686.jpg", + "format" : "jpeg" + } + ], + "worldId" : -1, + "likeCount" : 0, + "type" : 2, + "dynamicId" : 265, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1753181143000, + "status" : 0, + "content" : "大" + }, + { + "isLike" : false, + "uid" : 3184, + "playCount" : 0, + "dynamicResList" : [ + { + "height" : 3024, + "id" : 440, + "width" : 4032, + "resUrl" : "https:\/\/https:\/\/image.molistar.xyz\/images\/DF8E655B-2F63-4B34-90B3-13C8A812245C.jpg", + "format" : "jpeg" + }, + { + "height" : 1792, + "id" : 441, + "width" : 828, + "resUrl" : "https:\/\/https:\/\/image.molistar.xyz\/images\/D869C761-59CC-4E6B-BB2A-74F87D4A4979.jpg", + "format" : "jpeg" + } + ], + "worldId" : -1, + "likeCount" : 0, + "type" : 2, + "dynamicId" : 264, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1753180835000, + "status" : 0, + "content" : "好" + }, + { + "isLike" : false, + "uid" : 3184, + "playCount" : 0, + "dynamicResList" : [ + { + "height" : 1792, + "id" : 438, + "width" : 828, + "resUrl" : "https:\/\/image.molistar.xyz\/image\/9d5c8e10eb0d228a26ec4e8d58b41c38.jpeg", + "format" : "jpeg" + }, + { + "height" : 1792, + "id" : 439, + "width" : 828, + "resUrl" : "https:\/\/image.molistar.xyz\/image\/9ab8dff9f5ffbb4d65998822dd126794.jpeg", + "format" : "jpeg" + } + ], + "worldId" : -1, + "likeCount" : 0, + "type" : 2, + "dynamicId" : 263, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1753180130000, + "status" : 0, + "content" : "猜猜猜" + }, + { + "isLike" : false, + "uid" : 3184, + "playCount" : 0, + "worldId" : -1, + "likeCount" : 0, + "type" : 0, + "dynamicId" : 262, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1753168392000, + "status" : 0, + "content" : "他哥哥哥哥哥哥" + }, + { + "isLike" : false, + "uid" : 3184, + "playCount" : 0, + "worldId" : -1, + "likeCount" : 0, + "type" : 0, + "dynamicId" : 261, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1753168329000, + "status" : 0, + "content" : "一直以为自己是真的" + }, + { + "isLike" : false, + "uid" : 3184, + "playCount" : 0, + "worldId" : -1, + "likeCount" : 0, + "type" : 0, + "dynamicId" : 260, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1753167661000, + "status" : 0, + "content" : "在意那些是自己一" + }, + { + "isLike" : false, + "uid" : 3184, + "playCount" : 0, + "worldId" : -1, + "likeCount" : 0, + "type" : 0, + "dynamicId" : 259, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1753166596000, + "status" : 0, + "content" : "哈哈我觉得这个世界" + }, + { + "isLike" : false, + "uid" : 3184, + "playCount" : 0, + "worldId" : -1, + "likeCount" : 0, + "type" : 0, + "dynamicId" : 258, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1753166592000, + "status" : 0, + "content" : "哈哈我觉得这个世界" + }, + { + "isLike" : false, + "uid" : 3184, + "playCount" : 0, + "worldId" : -1, + "likeCount" : 0, + "type" : 0, + "dynamicId" : 257, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1753166298000, + "status" : 0, + "content" : "哈哈哈哈更" + }, + { + "isLike" : false, + "uid" : 3184, + "playCount" : 0, + "worldId" : -1, + "likeCount" : 0, + "type" : 0, + "dynamicId" : 256, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1753165531000, + "status" : 0, + "content" : "不不不不不" + }, + { + "isLike" : false, + "uid" : 3184, + "playCount" : 0, + "worldId" : -1, + "likeCount" : 0, + "type" : 0, + "dynamicId" : 255, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1753156105000, + "status" : 0, + "content" : "你有什么" + }, + { + "isLike" : false, + "uid" : 3184, + "playCount" : 0, + "worldId" : -1, + "likeCount" : 0, + "type" : 0, + "dynamicId" : 254, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1752650142000, + "status" : 0, + "content" : "igvigciycoyvcoyvyovoy突袭陶瓷陶瓷陶瓷陶瓷陶瓷陶瓷陶瓷" + }, + { + "isLike" : false, + "uid" : 3184, + "playCount" : 0, + "worldId" : -1, + "likeCount" : 0, + "type" : 0, + "dynamicId" : 247, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1742801936000, + "status" : 0, + "content" : "vicigiigohvhveerr让你表弟姐姐接你吧多半都不\n\n\n\n代表脯肉吧多半日品牌狠批人品很频频频频噢……在一起的时候就是一次又来了就可以😌!我们一起加油呀!我要好好学习📑!你好开心🥳、在一起的时候就像一只手握在一起\n" + }, + { + "isLike" : false, + "uid" : 3184, + "playCount" : 0, + "worldId" : -1, + "likeCount" : 0, + "type" : 0, + "dynamicId" : 206, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1726834519000, + "status" : 1, + "content" : "爸爸不会后悔就" + }, + { + "isLike" : true, + "uid" : 3184, + "playCount" : 0, + "dynamicResList" : [ + { + "height" : 500, + "id" : 355, + "width" : 500, + "resUrl" : "https:\/\/image.pekolive.com\/image\/0c091078d01305f3144ab3352a9fe21a.jpeg", + "format" : "jpeg" + }, + { + "height" : 328, + "id" : 356, + "width" : 440, + "resUrl" : "https:\/\/image.pekolive.com\/image\/8cdbbab3a0e6df7389f2d2671ee48bc3.jpeg", + "format" : "jpeg" + } + ], + "worldId" : -1, + "likeCount" : 1, + "type" : 2, + "dynamicId" : 205, + "nick" : "hansome", + "avatar" : "https:\/\/image.pekolive.com\/image\/023296a7d637e6406fb2aa19e967b607.jpeg", + "commentCount" : 0, + "publishTime" : 1726834050000, + "status" : 1, + "content" : "寄件人、一直以为自己是什么地方玩不" + } + ], + "timestamp" : 1753256129947 +} +===================================== \ No newline at end of file diff --git a/yana/Features/HomeFeature.swift b/yana/Features/HomeFeature.swift index 1cfa4b9..d99246e 100644 --- a/yana/Features/HomeFeature.swift +++ b/yana/Features/HomeFeature.swift @@ -1,33 +1,24 @@ import Foundation import ComposableArchitecture -@Reducer -struct HomeFeature { +struct HomeFeature: Reducer { enum Route: Equatable { case createFeed } - - @ObservableState - struct State: Equatable, Sendable { + + struct State: Equatable { var isInitialized = false var userInfo: UserInfo? var accountModel: AccountModel? var error: String? - - // 设置页面相关状态 var isSettingPresented = false var settingState = SettingFeature.State() - - // 新增:Feed 状态 var feedState = FeedFeature.State() - - // 新增:登出状态 + var meDynamic = MeDynamicFeature.State(uid: 0) var isLoggedOut = false - - // 新增:路由状态 var route: Route? = nil } - + @CasePathable enum Action: Equatable { case onAppear @@ -37,91 +28,77 @@ struct HomeFeature { case accountModelLoaded(AccountModel?) case logoutTapped case logout - - // 设置页面相关actions case settingDismissed case setting(SettingFeature.Action) - - // 新增:Feed actions case feed(FeedFeature.Action) - - // 新增:登出完成 + case meDynamic(MeDynamicFeature.Action) case logoutCompleted - - // 新增:路由 actions case showCreateFeed case createFeedDismissed } - - var body: some Reducer { -// Reducer.combine([ -// Reducer { state, action in -// switch action { -// case .onAppear: -// guard !state.isInitialized else { -// return Effect.none -// } -// state.isInitialized = true -// return .concatenate( -// .send(.loadUserInfo), -// .send(.loadAccountModel) -// ) -// case .loadUserInfo: -// return .run { send in -// let userInfo = await UserInfoManager.getUserInfo() -// await send(.userInfoLoaded(userInfo)) -// } -// case let .userInfoLoaded(userInfo): -// state.userInfo = userInfo -// return Effect.none -// case .loadAccountModel: -// return .run { send in -// let accountModel = await UserInfoManager.getAccountModel() -// await send(.accountModelLoaded(accountModel)) -// } -// case let .accountModelLoaded(accountModel): -// state.accountModel = accountModel -// return Effect.none -// case .logoutTapped: -// return .send(.logout) -// case .logout: -// return .run { send in -// await UserInfoManager.clearAllAuthenticationData() -// await send(.logoutCompleted) -// } -// case .logoutCompleted: -// state.isLoggedOut = true -// return Effect.none -// case .settingDismissed: -// state.isSettingPresented = false -// return Effect.none -// case .setting: -// return Effect.none -// case .showCreateFeed: -// state.route = .createFeed -// return Effect.none -// case .createFeedDismissed: -// state.route = nil -// return Effect.none -// case .feed: -// return Effect.none -// } -// }, -// Scope( -// state: \State.settingState, -// action: /Action.setting, -// child: SettingFeature() -// ), -// Scope( -// state: \State.feedState, -// action: /Action.feed, -// child: FeedFeature() -// ) -// ]) + + var body: some ReducerOf { + Scope(state: \.settingState, action: \.setting) { + SettingFeature() + } + Scope(state: \.feedState, action: \.feed) { + FeedFeature() + } + Scope(state: \.meDynamic, action: \.meDynamic) { + MeDynamicFeature() + } + Reduce { state, action in + switch action { + case .onAppear: + guard !state.isInitialized else { return .none } + state.isInitialized = true + return .concatenate( + .send(.loadUserInfo), + .send(.loadAccountModel) + ) + case .loadUserInfo: + return .run { send in + let userInfo = await UserInfoManager.getUserInfo() + await send(.userInfoLoaded(userInfo)) + } + case let .userInfoLoaded(userInfo): + state.userInfo = userInfo + state.meDynamic.uid = userInfo?.uid ?? 0 + return .none + case .loadAccountModel: + return .run { send in + let accountModel = await UserInfoManager.getAccountModel() + await send(.accountModelLoaded(accountModel)) + } + case let .accountModelLoaded(accountModel): + state.accountModel = accountModel + return .none + case .logoutTapped: + return .send(.logout) + case .logout: + return .run { send in + await UserInfoManager.clearAllAuthenticationData() + await send(.logoutCompleted) + } + case .logoutCompleted: + state.isLoggedOut = true + return .none + case .settingDismissed: + state.isSettingPresented = false + return .none + case .setting: + return .none + case .showCreateFeed: + state.route = .createFeed + return .none + case .createFeedDismissed: + state.route = nil + return .none + case .feed: + return .none + case .meDynamic: + return .none + } + } } } - -// 移除:未使用的通知名称定义 -// extension Notification.Name { -// static let homeLogout = Notification.Name("homeLogout") -// } diff --git a/yana/Features/MainFeature.swift b/yana/Features/MainFeature.swift index bf9a1d3..691f7ee 100644 --- a/yana/Features/MainFeature.swift +++ b/yana/Features/MainFeature.swift @@ -10,25 +10,51 @@ struct MainFeature: Reducer { struct State: Equatable { var selectedTab: Tab = .feed var feedList: FeedListFeature.State = .init() + var meDynamic: MeDynamicFeature.State = .init(uid: 0) + var accountModel: AccountModel? = nil } @CasePathable enum Action: Equatable { + case onAppear case selectTab(Tab) case feedList(FeedListFeature.Action) + case meDynamic(MeDynamicFeature.Action) + case accountModelLoaded(AccountModel?) } var body: some ReducerOf { Scope(state: \.feedList, action: \.feedList) { FeedListFeature() } + Scope(state: \.meDynamic, action: \.meDynamic) { + MeDynamicFeature() + } Reduce { state, action in switch action { + case .onAppear: + return .run { send in + let accountModel = await UserInfoManager.getAccountModel() + await send(.accountModelLoaded(accountModel)) + } case .selectTab(let tab): state.selectedTab = tab + if tab == .other, let uidStr = state.accountModel?.uid, let uid = Int(uidStr), uid > 0 { + state.meDynamic = MeDynamicFeature.State(uid: uid) + return .send(.meDynamic(.onAppear)) + } return .none case .feedList: return .none + case let .accountModelLoaded(accountModel): + state.accountModel = accountModel + if let uidStr = accountModel?.uid, let uid = Int(uidStr), uid > 0 { + state.meDynamic = MeDynamicFeature.State(uid: uid) + return .send(.meDynamic(.onAppear)) + } + return .none + default: + return .none } } } diff --git a/yana/Features/MeDynamicFeature.swift b/yana/Features/MeDynamicFeature.swift new file mode 100644 index 0000000..c9126a8 --- /dev/null +++ b/yana/Features/MeDynamicFeature.swift @@ -0,0 +1,91 @@ +import Foundation +import ComposableArchitecture + +@Reducer +struct MeDynamicFeature: Reducer { + struct State: Equatable { + var uid: Int + var dynamics: [MomentsInfo] = [] + var page: Int = 1 + var pageSize: Int = 20 + var isLoading: Bool = false + var isRefreshing: Bool = false + var isLoadingMore: Bool = false + var hasMore: Bool = true + var error: String? + var isInitialized: Bool = false // 首次加载标记 + } + + enum Action: Equatable { + case onAppear + case refresh + case loadMore + case fetchResponse(Result) + } + + @Dependency(\.apiService) var apiService + + func reduce(into state: inout State, action: Action) async -> Effect { + switch action { + case .onAppear: + guard !state.isInitialized else { return .none } + state.isInitialized = true + state.page = 1 + state.dynamics = [] + state.hasMore = true + state.isLoading = true + state.error = nil + return fetchDynamics(uid: state.uid, page: 1, pageSize: state.pageSize) + case .refresh: + state.page = 1 + state.hasMore = true + state.isRefreshing = true + state.error = nil + state.isInitialized = false // 允许刷新后重新加载 + return fetchDynamics( + uid: state.uid, + page: 1, + pageSize: state.pageSize + ) + case .loadMore: + guard !state.isLoadingMore, state.hasMore else { return .none } + state.isLoadingMore = true + return fetchDynamics(uid: state.uid, page: state.page + 1, pageSize: state.pageSize) + case let .fetchResponse(result): + state.isLoading = false + state.isRefreshing = false + state.isLoadingMore = false + switch result { + case let .success(resp): + let newDynamics = resp.data ?? [] + if state.page == 1 { + state.dynamics = newDynamics + } else { + state.dynamics += newDynamics + } + state.hasMore = newDynamics.count == state.pageSize + if state.hasMore { state.page += 1 } + state.error = nil + case let .failure(error): + state.error = error.localizedDescription + } + return .none + } + } + + private func fetchDynamics(uid: Int, page: Int, pageSize: Int) -> Effect { + let apiService = self.apiService + return .run { send in + do { + let req = GetMyDynamicRequest(fromUid: uid, uid: uid, page: page, pageSize: pageSize) + let resp = try await apiService.request(req) + await send(.fetchResponse(.success(resp))) + } catch { + await send(.fetchResponse(.failure(error as? APIError ?? .unknown(error.localizedDescription)))) + } + } + } +} + + + diff --git a/yana/Views/Components/OptimizedDynamicCardView.swift b/yana/Views/Components/OptimizedDynamicCardView.swift index d7a2fb9..2931741 100644 --- a/yana/Views/Components/OptimizedDynamicCardView.swift +++ b/yana/Views/Components/OptimizedDynamicCardView.swift @@ -71,7 +71,7 @@ struct OptimizedDynamicCardView: View { // 优化的图片网格 if let images = moment.dynamicResList, !images.isEmpty { OptimizedImageGrid(images: images) { tappedIndex in - previewImageUrls = images.map { $0.resUrl } + previewImageUrls = images.map { $0.resUrl ?? "" } previewIndex = tappedIndex showPreview = true } @@ -160,7 +160,7 @@ struct OptimizedDynamicCardView: View { let moment = allMoments[index] urlsToPreload.append(moment.avatar) if let images = moment.dynamicResList { - urlsToPreload.append(contentsOf: images.map { $0.resUrl }) + urlsToPreload.append(contentsOf: images.compactMap { $0.resUrl }) } } ImageCacheManager.shared.preloadImages(urls: urlsToPreload) @@ -277,7 +277,7 @@ struct SquareImageView: View { } private var imageContent: some View { - CachedAsyncImage(url: image.resUrl) { imageView in + CachedAsyncImage(url: image.resUrl ?? "") { imageView in imageView .resizable() .aspectRatio(contentMode: .fill) diff --git a/yana/Views/HomeView.swift b/yana/Views/HomeView.swift index 54a1389..9c07c4f 100644 --- a/yana/Views/HomeView.swift +++ b/yana/Views/HomeView.swift @@ -34,8 +34,15 @@ struct HomeView: View { ) .transition(.opacity) case .me: - MeView(onLogout: onLogout) - .transition(.opacity) + Spacer() +// MeView( +// meDynamicStore: store.scope( +// state: \.meDynamic, +// action: \.meDynamic +// ), +// onLogout: onLogout +// ) +// .transition(.opacity) } } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/yana/Views/MainView.swift b/yana/Views/MainView.swift index aabf791..22c3085 100644 --- a/yana/Views/MainView.swift +++ b/yana/Views/MainView.swift @@ -1,6 +1,5 @@ import SwiftUI import ComposableArchitecture -//import Components // 如果 BottomTabView 在 Components 命名空间,否则移除 struct MainView: View { let store: StoreOf @@ -27,9 +26,17 @@ struct MainView: View { )) .transition(.opacity) case .other: - - MeView(onLogout: {}) // 这里可根据需要传递实际登出回调 + if let accountModel = viewStore.accountModel { + MeView( + meDynamicStore: store.scope( + state: \.meDynamic, + action: \.meDynamic + ), + accountModel: accountModel, + onLogout: {} + ) .transition(.opacity) + } } } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -45,6 +52,9 @@ struct MainView: View { } } } + .onAppear { + viewStore.send(.onAppear) + } } } } diff --git a/yana/Views/MeDynamicView.swift b/yana/Views/MeDynamicView.swift new file mode 100644 index 0000000..9794a96 --- /dev/null +++ b/yana/Views/MeDynamicView.swift @@ -0,0 +1,75 @@ +import SwiftUI +import ComposableArchitecture + +struct MeDynamicView: View { + let store: StoreOf + // uid 由外部传入 + + @State private var showDeleteAlert = false + @State private var selectedMoment: MomentsInfo? + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Group { + if viewStore.isLoading && viewStore.dynamics.isEmpty { + ProgressView("加载中...") + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if let error = viewStore.error { + VStack(spacing: 16) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 40)) + .foregroundColor(.yellow) + Text(error) + .foregroundColor(.red) + Button("重试") { + viewStore.send(.onAppear) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if viewStore.dynamics.isEmpty { + VStack(spacing: 16) { + Image(systemName: "tray") + .font(.system(size: 40)) + .foregroundColor(.gray) + Text("暂无动态") + .foregroundColor(.white.opacity(0.8)) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + ScrollView { + LazyVStack(spacing: 12) { + ForEach(Array(viewStore.dynamics.enumerated()), id: \.element.dynamicId) { index, moment in + OptimizedDynamicCardView(moment: moment, allMoments: viewStore.dynamics, currentIndex: index) + .padding(.horizontal, 12) + .onLongPressGesture { + if viewStore.uid == moment.uid { + selectedMoment = moment + showDeleteAlert = true + } + } + } + if viewStore.hasMore { + ProgressView() + .onAppear { + viewStore.send(.loadMore) + } + } + } + .padding(.top, 8) + } + .refreshable { + viewStore.send(.refresh) + } + } + } + .alert("确认删除该动态?", isPresented: $showDeleteAlert, presenting: selectedMoment) { moment in + Button("删除", role: .destructive) { + // TODO: 后续可在此触发删除Action,如 viewStore.send(.delete(moment.dynamicId)) + } + Button("取消", role: .cancel) {} + } message: { _ in + Text("此操作不可恢复") + } + } + } +} diff --git a/yana/Views/MeView.swift b/yana/Views/MeView.swift index 41cfdad..afd36c7 100644 --- a/yana/Views/MeView.swift +++ b/yana/Views/MeView.swift @@ -2,90 +2,48 @@ import SwiftUI import ComposableArchitecture struct MeView: View { + let meDynamicStore: StoreOf + let accountModel: AccountModel @State private var showLogoutConfirmation = false - let onLogout: () -> Void // 新增:登出回调 - @State private var showSetting = false // 新增:控制SettingView弹出 + @State private var showSetting = false + @State private var userInfo: UserInfo? + @State private var isLoadingUserInfo = true + @State private var errorMessage: String? + @State private var hasLoaded = false + + let onLogout: () -> Void var body: some View { GeometryReader { geometry in - ScrollView { - VStack(spacing: 20) { - // 顶部标题 - HStack { - Spacer() - Text("我的") - .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) - - // 用户头像区域 - VStack(spacing: 16) { - Circle() - .fill(Color.white.opacity(0.2)) - .frame(width: 80, height: 80) - .overlay( - Image(systemName: "person.fill") - .font(.system(size: 40)) - .foregroundColor(.white) - ) - - Text("用户昵称") - .font(.system(size: 18, weight: .medium)) - .foregroundColor(.white) - - Text("ID: 123456789") - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.7)) - } - .padding(.top, 30) - - // 功能菜单 - VStack(spacing: 12) { - MenuItemView(icon: "gearshape", title: "设置", action: {}) - MenuItemView(icon: "person.circle", title: "个人信息", action: {}) - MenuItemView(icon: "heart", title: "我的收藏", action: {}) - MenuItemView(icon: "clock", title: "浏览历史", action: {}) - MenuItemView(icon: "questionmark.circle", title: "帮助与反馈", action: {}) - } - .padding(.horizontal, 20) - .padding(.top, 40) - - // 退出登录按钮 - Button(action: { - showLogoutConfirmation = true - }) { - HStack { - Image(systemName: "rectangle.portrait.and.arrow.right") - .font(.system(size: 16)) - Text("退出登录") - .font(.system(size: 16, weight: .medium)) - } - .foregroundColor(.red) - .frame(maxWidth: .infinity) - .frame(height: 50) - .background( - Color.white.opacity(0.1) - .cornerRadius(12) - ) - } - .padding(.horizontal, 20) - .padding(.top, 30) - - // 底部安全区域 - 为底部导航栏和安全区域留出空间 - Color.clear.frame(height: geometry.safeAreaInsets.bottom + 100) + ZStack { + // 背景图片 - 使用现有的"bg"图片 + Image("bg") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipped() + .ignoresSafeArea(.all) + + VStack(spacing: 0) { + // 用户信息区域 - 固定位置 + UserProfileSection( + userInfo: userInfo, + isLoading: isLoadingUserInfo, + errorMessage: errorMessage, + onSettingTapped: { showSetting = true } + ) + // 动态内容区域 + MeDynamicView(store: meDynamicStore) + .frame(maxWidth: .infinity, maxHeight: .infinity) } - .padding(.top, 100) } } - .ignoresSafeArea(.container, edges: .top) + .onAppear { + if !hasLoaded { + loadUserInfo() + hasLoaded = true + } + } .alert("确认退出", isPresented: $showLogoutConfirmation) { Button("取消", role: .cancel) { } Button("退出", role: .destructive) { @@ -95,53 +53,178 @@ struct MeView: View { Text("确定要退出登录吗?") } .sheet(isPresented: $showSetting) { - // 这里用预设store,实际项目可替换为真实store SettingView(store: Store(initialState: SettingFeature.State()) { SettingFeature() }) } } + // MARK: - 加载用户信息 + private func loadUserInfo() { + Task { + isLoadingUserInfo = true + errorMessage = nil + + debugInfoSync("📱 MeView: 开始加载用户信息") + + // 获取当前用户ID + guard let currentUserId = await UserInfoManager.getCurrentUserId() else { + debugErrorSync("❌ MeView: 无法获取当前用户ID") + await MainActor.run { + errorMessage = "用户未登录" + isLoadingUserInfo = false + } + return + } + + debugInfoSync("📱 MeView: 当前用户ID: \(currentUserId)") + + // 创建APIService实例 + let apiService: APIServiceProtocol = LiveAPIService() + + // 先尝试从本地缓存获取 + if let cachedUserInfo = await UserInfoManager.getUserInfo() { + debugInfoSync("📱 MeView: 使用本地缓存的用户信息") + await MainActor.run { + self.userInfo = cachedUserInfo + self.isLoadingUserInfo = false + } + } + + // 然后从服务器获取最新数据 + debugInfoSync("🌐 MeView: 从服务器获取最新用户信息") + let freshUserInfo = await UserInfoManager.fetchUserInfoFromServer( + uid: currentUserId, + apiService: apiService + ) + + await MainActor.run { + if let freshUserInfo = freshUserInfo { + debugInfoSync("✅ MeView: 成功获取最新用户信息") + debugInfoSync(" 用户名: \(freshUserInfo.nick ?? freshUserInfo.nick ?? "未知")") + debugInfoSync(" 用户ID: \(String(freshUserInfo.uid ?? 0))") + self.userInfo = freshUserInfo + self.errorMessage = nil + } else { + debugErrorSync("❌ MeView: 无法从服务器获取用户信息") + if self.userInfo == nil { + self.errorMessage = "无法获取用户信息" + } + } + self.isLoadingUserInfo = false + } + } + } + // MARK: - 退出登录方法 private func performLogout() async { debugInfoSync("🔓 开始执行退出登录...") - // 清除所有认证数据(包括 keychain 中的内容) await UserInfoManager.clearAllAuthenticationData() - // 调用登出回调,通知父级切换视图 onLogout() debugInfoSync("✅ 退出登录完成") } + + // MARK: - 设置按钮点击 + private func onSettingTapped() { + showSetting = true + } + + // MARK: - 复制用户ID + private func copyUserId() { + if let userId = userInfo?.userId { + UIPasteboard.general.string = userId + debugInfoSync("📋 MeView: 用户ID已复制到剪贴板: \(userId)") + } + } } -// MARK: - 菜单项组件 -struct MenuItemView: View { - let icon: String - let title: String - let action: () -> Void +// MARK: - 用户信息区域组件 +struct UserProfileSection: View { + let userInfo: UserInfo? + let isLoading: Bool + let errorMessage: String? + let onSettingTapped: () -> Void var body: some View { - Button(action: action) { - HStack(spacing: 16) { - Image(systemName: icon) - .font(.system(size: 20)) - .foregroundColor(.white) - .frame(width: 24) - - Text(title) - .font(.system(size: 16)) - .foregroundColor(.white) - .frame(maxWidth: .infinity, alignment: .leading) - - Image(systemName: "chevron.right") - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.6)) + VStack(spacing: 16) { + // 顶部栏:设置按钮 + HStack { + Spacer() + Button(action: onSettingTapped) { + Image(systemName: "gearshape") + .font(.system(size: 22, weight: .regular)) + .foregroundColor(.white) + } + .padding(.trailing, 20) + } + + // 用户头像 + if isLoading { + Circle() + .fill(Color.white.opacity(0.2)) + .frame(width: 130, height: 130) + .overlay( + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + ) + } else { + Circle() + .fill(Color.white.opacity(0.2)) + .frame(width: 130, height: 130) + .overlay( + Group { + if let avatarUrl = userInfo?.avatar, !avatarUrl.isEmpty { + AsyncImage(url: URL(string: avatarUrl)) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + .clipShape(Circle()) + } placeholder: { + Image(systemName: "person.fill") + .font(.system(size: 40)) + .foregroundColor(.white) + } + } else { + Image(systemName: "person.fill") + .font(.system(size: 40)) + .foregroundColor(.white) + } + } + ) + } + + // 用户名称 + if let errorMessage = errorMessage { + Text(errorMessage) + .font(.system(size: 14)) + .foregroundColor(.red) + .multilineTextAlignment(.center) + } else { + Text(userInfo?.nick ?? userInfo?.nick ?? "用户昵称") + .font(.system(size: 18, weight: .medium)) + .foregroundColor(.white) + } + + // 用户ID + HStack(spacing: 8) { + Text("ID: \(userInfo?.uid ?? 0)") + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.7)) + + if userInfo?.userId != nil { + Button(action: { + // 复制用户ID到剪贴板 + if let userId = userInfo?.userId { + UIPasteboard.general.string = userId + } + }) { + Image(systemName: "doc.on.doc") + .font(.system(size: 12)) + .foregroundColor(.white.opacity(0.6)) + } + } } - .padding(.horizontal, 20) - .frame(height: 56) - .background( - Color.white.opacity(0.1) - .cornerRadius(12) - ) } - .buttonStyle(PlainButtonStyle()) + .padding(.horizontal, 20) + .padding(.bottom, 20) } } diff --git a/项目问题排查与解决流程.md b/项目问题排查与解决流程.md index 1e14f19..61efe70 100644 --- a/项目问题排查与解决流程.md +++ b/项目问题排查与解决流程.md @@ -175,11 +175,13 @@ xcodebuild -workspace yana.xcworkspace -scheme yana -configuration Debug build ``` ### 关键代码修改 + 1. **HomeFeature.swift**: 添加设置相关状态管理 2. **HomeView.swift**: 修复 TCA store 绑定语法 3. **SettingFeature.swift**: 确保 Action 完整性 ### 构建结果 + ✅ **编译成功**: Exit code 0 ⚠️ **警告信息**: 仅 Swift 6 兼容性警告,不影响运行 @@ -188,18 +190,21 @@ xcodebuild -workspace yana.xcworkspace -scheme yana -configuration Debug build ## 预防措施 ### 开发规范 + 1. **统一包管理**: 优先使用一种包管理工具 2. **定期清理**: 定期清理 DerivedData 避免缓存问题 3. **代码审查**: 确保 TCA Feature 结构完整 4. **版本控制**: 及时提交关键配置文件 ### 监控指标 + - [ ] 项目编译时间 < 30s - [ ] 无编译错误 - [ ] 依赖解析正常 - [ ] TCA 结构完整 ### 工具使用 + ```bash # 项目健康检查脚本 check_project() { @@ -255,6 +260,7 @@ pod install --clean-install ## 总结 本次问题解决涉及以下关键技术点: + 1. **Xcode 项目配置管理** 2. **Swift Package Manager 与 CocoaPods 共存** 3. **TCA (The Composable Architecture) 最佳实践** @@ -265,5 +271,5 @@ pod install --clean-install --- **文档更新时间**: 2025-07-10 -**适用版本**: iOS 15.6+, Swift 6, TCA 1.20.2+ -**维护者**: AI Assistant & 开发团队 \ No newline at end of file +**适用版本**: iOS 16+, Swift 6, TCA 1.20.2+ +**维护者**: AI Assistant & 开发团队 \ No newline at end of file