wangdl 82bcbb9efb feat: iOS 学习功能增强 - 阅读API、材料阅读器、分析、学习首页
- ReadingAPI: 阅读进度同步
- MaterialPathResolver: 材料路径解析
- MarkedReadStore: 已读标记存储
- MaterialReaderView/DetailView/ReadingPositionAdapter: 阅读器增强
- AnalysisHomeView/ActivityViewModel: 分析首页
- StudyHomeView/StudyHomeViewModel: 学习首页
- LibrarySubpages: 图书馆子页面

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 11:22:26 +08:00

132 lines
3.9 KiB
Swift

import Foundation
// MARK: - Reading API Service
@MainActor
class ReadingAPIService {
static let shared = ReadingAPIService()
private let client = APIClient.shared
/// POST /learning/reading-events/batch
func uploadReadingEvents(_ events: [ReadingEventUploadItem]) async throws -> ReadingEventBatchResponse {
let body = ReadingEventBatchRequest(events: events)
return try await client.request("/learning/reading-events/batch", method: "POST", body: body)
}
/// GET /materials/:id/reading-progress
func getReadingProgress(materialId: String, targetType: String) async throws -> MaterialReadingProgressDTO {
return try await client.request(
"/materials/\(materialId)/reading-progress",
queryItems: [URLQueryItem(name: "readingTargetType", value: targetType)]
)
}
/// GET /learning/continue
func getContinueLearning() async throws -> ContinueLearningResponse {
return try await client.request("/learning/continue")
}
/// GET /learning/summary
func getLearningSummary() async throws -> LearningSummaryResponse {
return try await client.request("/learning/summary")
}
/// GET /learning/trend?days=7
func getLearningTrend(days: Int = 7) async throws -> LearningTrendResponse {
return try await client.request(
"/learning/trend",
queryItems: [URLQueryItem(name: "days", value: String(days))]
)
}
/// GET /learning/records
func getLearningRecords(cursor: String? = nil, limit: Int = 20, type: String? = nil) async throws -> LearningRecordsResponse {
var items = [URLQueryItem(name: "limit", value: String(limit))]
if let c = cursor { items.append(URLQueryItem(name: "cursor", value: c)) }
if let t = type { items.append(URLQueryItem(name: "type", value: t)) }
return try await client.request("/learning/records", queryItems: items)
}
}
// MARK: - Response DTOs
/// Codable wrapper for API's lastPosition (JSON object matching the server's ReadingPosition schema).
struct ReadingPositionValue: Codable {
let type: String?
let blockId: String?
let lineNumber: UInt32?
let pageNumber: UInt32?
let chapterId: String?
let scrollProgress: Float?
let pageProgress: Float?
let overallProgress: Float?
let zoomScale: Float?
let offsetX: Float?
let offsetY: Float?
let chapterProgress: Float?
}
struct MaterialReadingProgressDTO: Codable {
let status: String
let lastPosition: ReadingPositionValue?
let lastProgress: Float?
let totalActiveSeconds: Int
let isMarkedRead: Bool
let sessionCount: Int?
let firstOpenedAt: String?
let lastReadAt: String?
}
struct ContinueLearningResponse: Codable {
let type: String?
let materialId: String?
let title: String?
let lastProgress: Float?
let totalActiveSeconds: Int?
let lastReadAt: String?
}
struct LearningSummaryResponse: Codable {
let todaySeconds: Int
let weekSeconds: Int
let totalSeconds: Int
let activeDays: Int
let sessionsCount: Int
let materialsReadCount: Int
let markedReadCount: Int
let dailyAverageSeconds: Int
}
struct LearningTrendResponse: Codable {
let days: Int
let series: [TrendPoint]
struct TrendPoint: Codable {
let date: String
let value: Int
}
}
struct LearningRecordsResponse: Codable {
let items: [RecordItem]
let nextCursor: String?
struct RecordItem: Codable {
let id: String
let recordType: String
let title: String
let description: String?
let durationSeconds: Int
let occurredAt: String
let metadata: RecordMetadata?
let createdAt: String
}
struct RecordMetadata: Codable {
let materialId: String?
let readingTargetType: String?
let knowledgeBaseId: String?
let totalActiveSeconds: Int?
}
}