feat: 更新动态功能,新增我的动态视图及相关状态管理
- 在HomeFeature中添加MeDynamicFeature以管理用户动态状态。 - 在MainFeature中集成MeDynamicFeature,支持动态内容的加载与展示。 - 新增MeDynamicView以展示用户的动态列表,支持下拉刷新和上拉加载更多功能。 - 更新MeView以集成用户动态视图,提升用户体验。 - 在APIEndpoints中新增getMyDynamic端点以支持获取用户动态信息。 - 更新DynamicsModels以适应新的动态数据结构,确保数据解析的准确性。 - 在OptimizedDynamicCardView中优化图片处理逻辑,提升动态展示效果。 - 更新相关视图组件以支持动态内容的展示与交互,增强用户体验。
This commit is contained in:
@@ -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.
|
||||
|
@@ -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)";
|
||||
|
@@ -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"
|
||||
|
@@ -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 }
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
||||
|
@@ -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 <CommonCrypto/CommonCrypto.h>`
|
||||
|
||||
### 🚀 性能提升效果
|
||||
|
||||
| 优化前 | 优化后 |
|
||||
|--------|--------|
|
||||
| ❌ 每次滚动重新加载图片 | ✅ 缓存图片瞬间显示 |
|
||||
| ❌ 频繁网络请求 | ✅ 大幅减少网络请求 |
|
||||
| ❌ 用户体验差 | ✅ 流畅滚动体验 |
|
||||
|
||||
### 📋 项目上下文回顾
|
||||
|
||||
1. **API 功能已完成**:
|
||||
- 动态内容 API 集成完毕(DynamicsModels.swift + FeedFeature.swift)
|
||||
- 数据解析问题已解决(类型匹配修复)
|
||||
- TCA 架构状态管理正常工作
|
||||
|
||||
2. **当前状态**:
|
||||
- ✅ 编译成功
|
||||
- ✅ API 数据正常显示
|
||||
- ✅ 图片缓存系统就绪
|
||||
- ✅ 性能优化完成
|
||||
|
||||
### 🔍 可能的后续工作
|
||||
|
||||
用户可能需要:
|
||||
1. **功能扩展**:点赞、评论、分享等交互功能
|
||||
2. **UI 优化**:更丰富的动画效果、主题切换
|
||||
3. **性能监控**:添加缓存命中率统计、内存使用监控
|
||||
4. **错误处理**:网络异常时的重试机制优化
|
||||
|
||||
### 💡 重要提醒
|
||||
|
||||
- **用户是中文开发者**:需要中文回复,使用 Chain-of-Thought 思考过程
|
||||
- **项目基于 iOS 15.6**:注意兼容性要求
|
||||
- **TCA 架构**:遵循项目现有的 TCA 模式
|
||||
- **图片缓存系统**:已经是生产就绪的企业级方案,无需重构
|
||||
|
||||
### 🎉 工作成果
|
||||
|
||||
这次优化彻底解决了图片重复加载的性能问题,用户现在可以享受流畅的滚动体验。缓存系统设计完善,支持大规模图片内容,为后续功能扩展奠定了坚实基础。
|
||||
|
||||
**继任者,你接手的是一个功能完整、性能优秀的动态内容展示系统!** 🚀
|
||||
|
||||
祝你工作顺利!
|
||||
📦 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
|
||||
}
|
||||
=====================================
|
@@ -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<State, Action> {
|
||||
// Reducer<State, Action>.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<Self> {
|
||||
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")
|
||||
// }
|
||||
|
@@ -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<Self> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
91
yana/Features/MeDynamicFeature.swift
Normal file
91
yana/Features/MeDynamicFeature.swift
Normal file
@@ -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<MyMomentsResponse, APIError>)
|
||||
}
|
||||
|
||||
@Dependency(\.apiService) var apiService
|
||||
|
||||
func reduce(into state: inout State, action: Action) async -> Effect<Action> {
|
||||
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<Action> {
|
||||
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))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
//import Components // 如果 BottomTabView 在 Components 命名空间,否则移除
|
||||
|
||||
struct MainView: View {
|
||||
let store: StoreOf<MainFeature>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
75
yana/Views/MeDynamicView.swift
Normal file
75
yana/Views/MeDynamicView.swift
Normal file
@@ -0,0 +1,75 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct MeDynamicView: View {
|
||||
let store: StoreOf<MeDynamicFeature>
|
||||
// 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("此操作不可恢复")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,90 +2,48 @@ import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct MeView: View {
|
||||
let meDynamicStore: StoreOf<MeDynamicFeature>
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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 & 开发团队
|
||||
**适用版本**: iOS 16+, Swift 6, TCA 1.20.2+
|
||||
**维护者**: AI Assistant & 开发团队
|
Reference in New Issue
Block a user