- ReadingAPI: 阅读进度同步 - MaterialPathResolver: 材料路径解析 - MarkedReadStore: 已读标记存储 - MaterialReaderView/DetailView/ReadingPositionAdapter: 阅读器增强 - AnalysisHomeView/ActivityViewModel: 分析首页 - StudyHomeView/StudyHomeViewModel: 学习首页 - LibrarySubpages: 图书馆子页面 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
132 lines
3.9 KiB
Swift
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?
|
|
}
|
|
}
|