feat(ios): 知识库列表卡片显示封面图

- ZLibraryCard 改用 coverUrl 显示封面图(56×56)
- 无封面时回退到默认书本图标
- KnowledgeBase 模型新增 coverUrl 字段

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-05-28 10:48:35 +08:00
parent d77a7fe50d
commit 587b61d564
2 changed files with 29 additions and 3 deletions

View File

@ -164,6 +164,7 @@ struct KnowledgeBase: Codable, Identifiable {
let title: String let title: String
let description: String? let description: String?
let coverKey: String? let coverKey: String?
let coverUrl: String?
let status: String? let status: String?
let itemCount: Int? let itemCount: Int?
let lastStudiedAt: String? let lastStudiedAt: String?

View File

@ -36,7 +36,7 @@ struct LibraryHomeView: View {
} }
ForEach(viewModel.knowledgeBases) { kb in ForEach(viewModel.knowledgeBases) { kb in
NavigationLink(value: Route.libraryDetail(knowledgeBaseId: kb.id)) { NavigationLink(value: Route.libraryDetail(knowledgeBaseId: kb.id)) {
ZLibraryCard(icon: "books.vertical.fill", name: kb.title, desc: kb.description ?? "", color: Color.zxPurple, items: kb.itemCount ?? 0, mastery: 50, tags: [], last: lastStudiedText(kb.lastStudiedAt)) ZLibraryCard(coverUrl: kb.coverUrl, name: kb.title, desc: kb.description ?? "", items: kb.itemCount ?? 0, last: lastStudiedText(kb.lastStudiedAt))
} }
} }
if viewModel.knowledgeBases.isEmpty && !viewModel.isLoading { if viewModel.knowledgeBases.isEmpty && !viewModel.isLoading {
@ -59,8 +59,33 @@ struct LibraryHomeView: View {
return iso.prefix(10).description return iso.prefix(10).description
} }
} }
struct ZLibraryCard: View { let icon: String; let name: String; let desc: String; let color: Color; let items: Int; let mastery: Int; let tags: [String]; let last: String struct ZLibraryCard: View { let coverUrl: String?; let name: String; let desc: String; let items: Int; let last: String
var body: some View { VStack(spacing: 0) { Rectangle().fill(ZXGradient.progressBar).frame(height: 3); HStack(spacing: 12) { Image(systemName: icon).font(.system(size: 20)).foregroundColor(color).frame(width: 44, height: 44).background(color.opacity(0.12)).clipShape(RoundedRectangle(cornerRadius: 13)).overlay(RoundedRectangle(cornerRadius: 13).stroke(color.opacity(0.3), lineWidth: 1)); VStack(alignment: .leading, spacing: 2) { Text(name).font(.system(size: 16, weight: .bold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04); Text("掌握 \(mastery)%").font(.system(size: 11)).foregroundColor(Color.zxF04) }; Spacer() }.padding(16); HStack { HStack(spacing: 4) { Image(systemName: "clock").font(.system(size: 10)); Text("\(items) 项 · \(last)").font(.system(size: 11)) }.foregroundColor(Color.zxF03); Spacer(); ForEach(tags.prefix(2), id: \.self) { t in Text(t).font(.system(size: 10, weight: .medium)).foregroundColor(Color.zxPurple).padding(.horizontal, 7).padding(.vertical, 2).background(Color(hex: "#7C6EFA", opacity: 0.08)).clipShape(Capsule()) } }.padding(.horizontal, 16).padding(.bottom, 12) } var body: some View { VStack(spacing: 0) {
HStack(spacing: 12) {
//
ZStack {
RoundedRectangle(cornerRadius: 13).fill(Color.zxPurpleBG(0.12)).frame(width: 56, height: 56)
if let url = coverUrl, let imageUrl = URL(string: url) {
AsyncImage(url: imageUrl) { phase in
switch phase {
case .success(let img): img.resizable().scaledToFill().frame(width: 56, height: 56).clipShape(RoundedRectangle(cornerRadius: 13))
default: Image(systemName: "books.vertical.fill").font(.system(size: 22)).foregroundColor(Color.zxPurple.opacity(0.5))
}
}
} else {
Image(systemName: "books.vertical.fill").font(.system(size: 22)).foregroundColor(Color.zxPurple.opacity(0.5))
}
}
VStack(alignment: .leading, spacing: 4) {
Text(name).font(.system(size: 16, weight: .bold)).foregroundColor(Color.zxF0)
if !desc.isEmpty { Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04).lineLimit(1) }
}
Spacer()
}.padding(16)
HStack {
HStack(spacing: 4) { Image(systemName: "clock").font(.system(size: 10)); Text("\(items) 项 · \(last)").font(.system(size: 11)) }.foregroundColor(Color.zxF03)
Spacer()
}.padding(.horizontal, 16).padding(.bottom, 12) }
.background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 20)).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) } .background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 20)).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) }
} }