From 1da412e637672e2d061107100e584bddc9c7126b Mon Sep 17 00:00:00 2001 From: YANGJIANKUAN Date: Fri, 27 Feb 2026 11:33:22 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E9=A1=B5=E7=A9=BA=E7=99=BD=E5=92=8C=E6=B5=B7=E6=8A=A5=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DetailViewModel 添加 @MainActor 确保状态更新在主线程 - DetailView 消除空白初始状态,ProgressView 作为默认兜底 - 取消时保留加载状态避免页面闪回空白 - 使用 .task(id:) 确保切换条目时任务重新触发 - 海报优先从 JSON-LD image 字段获取,HTML fallback 改用正确选择器 Co-Authored-By: Claude Opus 4.6 --- DDYSClient/Services/HTMLParser.swift | 15 +++++++++++---- DDYSClient/ViewModels/DetailViewModel.swift | 15 +++++++-------- DDYSClient/Views/Detail/DetailView.swift | 14 +++++++------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/DDYSClient/Services/HTMLParser.swift b/DDYSClient/Services/HTMLParser.swift index d4667d3..61e11e1 100644 --- a/DDYSClient/Services/HTMLParser.swift +++ b/DDYSClient/Services/HTMLParser.swift @@ -93,10 +93,6 @@ enum HTMLParser { let h1 = try doc.select("h1").first() let fullTitle = try h1?.text().trimmingCharacters(in: .whitespacesAndNewlines) ?? "未知标题" - // 海报 - let imgSrc = try doc.select("img.w-full.h-full.object-cover").first()?.attr("src") ?? "" - let posterURL = URL(string: imgSrc) - // === 优先从 JSON-LD 提取结构化数据 === var year = 0 var rating: Double? @@ -105,12 +101,17 @@ enum HTMLParser { var genres: [String] = [] var description = "" var region = "" + var posterURL: URL? if let jsonLDScript = try doc.select("script[type=application/ld+json]").first() { let jsonText = try jsonLDScript.data() if let data = jsonText.data(using: .utf8), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { + if let imageStr = json["image"] as? String { + posterURL = URL(string: imageStr) + } + if let published = json["datePublished"] as? String { year = Int(published) ?? 0 } @@ -140,6 +141,12 @@ enum HTMLParser { // === 从 HTML 补充缺失数据 === + // 海报 fallback: 主海报在 .flex-shrink-0 容器中 + if posterURL == nil { + let imgSrc = try doc.select(".flex-shrink-0 img.object-cover").first()?.attr("src") ?? "" + posterURL = URL(string: imgSrc) + } + // 评分 fallback if rating == nil { let ratingText = try doc.select(".rating-display").text() diff --git a/DDYSClient/ViewModels/DetailViewModel.swift b/DDYSClient/ViewModels/DetailViewModel.swift index 0e228fb..74d95a1 100644 --- a/DDYSClient/ViewModels/DetailViewModel.swift +++ b/DDYSClient/ViewModels/DetailViewModel.swift @@ -1,6 +1,6 @@ import Foundation -@Observable +@Observable @MainActor final class DetailViewModel { var detail: ContentDetail? var selectedSourceIndex = 0 @@ -27,23 +27,22 @@ final class DetailViewModel { func loadDetail(path: String) async { isLoading = true error = nil - defer { isLoading = false } do { let html = try await APIClient.shared.fetchDetailPage(path: path) let parsedDetail = try HTMLParser.parseContentDetail(html: html) - await MainActor.run { - self.detail = parsedDetail - self.selectedSourceIndex = 0 - self.selectedEpisodeIndex = 0 - } + self.detail = parsedDetail + self.selectedSourceIndex = 0 + self.selectedEpisodeIndex = 0 } catch is CancellationError { + // 被取消时不清除 loading 状态,让视图继续显示加载中 return } catch let error as URLError where error.code == .cancelled { return } catch { - await MainActor.run { self.error = error.localizedDescription } + self.error = error.localizedDescription } + isLoading = false } func selectSource(_ index: Int) { diff --git a/DDYSClient/Views/Detail/DetailView.swift b/DDYSClient/Views/Detail/DetailView.swift index aa48d04..e581c01 100644 --- a/DDYSClient/Views/Detail/DetailView.swift +++ b/DDYSClient/Views/Detail/DetailView.swift @@ -12,22 +12,22 @@ struct DetailView: View { var body: some View { Group { - if viewModel.isLoading { - ProgressView() - .frame(maxWidth: .infinity, maxHeight: .infinity) - } else if let error = viewModel.error { - errorView(error) - } else if let detail = viewModel.detail { + if let detail = viewModel.detail { ScrollView { detailContent(detail) } + } else if let error = viewModel.error { + errorView(error) + } else { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) } } .navigationTitle(item.title) #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif - .task { + .task(id: item.id) { if viewModel.detail == nil { await viewModel.loadDetail(path: item.detailURL) }