feat(IOS-INFO-043): add 详情 and 整理知识点 toolbar buttons to MaterialReaderView

- MaterialReaderView: add info.circle button → MaterialDetailView
- MaterialReaderView: add brain button → trigger parse API + status polling
- MaterialReaderView: add triggerParse() with 2s poll loop
- KnowledgeSourceService: add parse() method (POST /sources/:id/parse)
- ImportStatusResponse: add jobId, progress fields
- ParseResponse: new model for parse trigger response

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-19 11:26:13 +08:00
parent fbf359ffe9
commit 123efb1612
4 changed files with 66 additions and 0 deletions

View File

@ -577,13 +577,21 @@ struct CreateImportRequest: Codable {
struct ImportStatusResponse: Codable {
let id: String
let jobId: String?
let status: String?
let fileName: String?
let knowledgeBaseId: String?
let errorMessage: String?
let progress: Int?
let itemCount: Int?
}
struct ParseResponse: Codable {
let id: String?
let jobId: String?
let status: String?
}
// MARK: - Import Candidate
struct ImportCandidate: Codable, Identifiable {

View File

@ -546,6 +546,10 @@ class KnowledgeSourceService {
func delete(kbId: String, id: String) async throws -> GenericSuccessResponse {
return try await client.request("/knowledge-bases/\(kbId)/sources/\(id)", method: "DELETE")
}
func parse(kbId: String, sourceId: String) async throws -> ParseResponse {
return try await client.request("/knowledge-bases/\(kbId)/sources/\(sourceId)/parse", method: "POST")
}
}
// MARK: - Quiz

View File

@ -107,6 +107,8 @@ struct MaterialReaderView: View {
@State private var apiRestorePosition: ReadingPosition?
@Environment(\.scenePhase) private var scenePhase
@State private var isParsing = false
@State private var parseMessage = ""
private let title: String
private let knowledgeBaseId: String?
@ -191,6 +193,19 @@ struct MaterialReaderView: View {
}
if let kbId = knowledgeBaseId {
NavigationLink(value: Route.materialDetail(
knowledgeBaseId: kbId,
fileName: title.isEmpty ? "资料" : title,
fileType: vm.materialType,
fileSize: 0,
filePath: vm.filePath,
uploadDate: nil
)) {
Image(systemName: "info.circle")
.font(.system(size: 16))
.foregroundColor(Color.zxF05)
}
NavigationLink(value: Route.learningSession(
taskTitle: title.isEmpty ? "学习资料" : title,
taskType: "study",
@ -200,6 +215,14 @@ struct MaterialReaderView: View {
.font(.system(size: 16))
.foregroundColor(Color.zxF05)
}
Button {
Task { await triggerParse(kbId: kbId, sourceId: vm.materialId) }
} label: {
Image(systemName: "brain.head.profile")
.font(.system(size: 16))
.foregroundColor(Color.zxF05)
}
}
}
}
@ -708,6 +731,37 @@ private func formatDuration(_ seconds: Int) -> String {
return "\(seconds / 3600)h\(String(format: "%02d", (seconds % 3600) / 60))m"
}
private func triggerParse(kbId: String, sourceId: String) async {
guard !isParsing else { return }
isParsing = true
parseMessage = "正在整理知识点..."
print("[MaterialReader] triggerParse START kbId=\(kbId) sourceId=\(sourceId)")
do {
let result = try await KnowledgeSourceService.shared.parse(kbId: kbId, sourceId: sourceId)
print("[MaterialReader] parse queued: id=\(result.jobId ?? "?"), status=\(result.status ?? "?")")
parseMessage = "知识点整理任务已提交"
// Poll status
for _ in 0..<60 {
try await Task.sleep(nanoseconds: 2_000_000_000)
let status = try await DocumentImportService.shared.getStatus(id: result.jobId ?? result.id ?? "")
print("[MaterialReader] parse status: \(status.status ?? "?")")
if status.status == "completed" {
parseMessage = "知识点整理完成"
break
} else if status.status == "failed" {
parseMessage = "整理失败: \(status.errorMessage ?? "未知错误")"
break
} else {
parseMessage = "整理中... \(status.progress ?? 0)%"
}
}
} catch {
print("[MaterialReader] triggerParse ERROR: \(error.localizedDescription)")
parseMessage = "整理失败: \(error.localizedDescription)"
}
isParsing = false
}
private func formatFileSize(_ bytes: UInt64) -> String {
if bytes < 1024 { return "\(bytes) B" }
if bytes < 1024 * 1024 { return String(format: "%.1f KB", Double(bytes) / 1024) }