feat(ios): IOS-M1-02 知识库列表筛选 + 置顶 + 排序
- LibraryHomeView 新增 filter chips(全部/我的/已订阅/官方) - ZLibraryCard 显示置顶图标 + 公开标签 - LibraryViewModel 新增 currentFilter + fetchKBs 按类型加载 - KnowledgeBaseService 新增 listSubscribed() - KnowledgeBase 模型新增 isPinned/visibility/ownerType/isVerified Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1162eaac07
commit
657e9cf2b4
@ -165,6 +165,11 @@ struct KnowledgeBase: Codable, Identifiable {
|
||||
let description: String?
|
||||
let coverKey: String?
|
||||
let coverUrl: String?
|
||||
let coverType: String?
|
||||
let visibility: String?
|
||||
let isPinned: Bool?
|
||||
let ownerType: String?
|
||||
let isVerified: Bool?
|
||||
let status: String?
|
||||
let itemCount: Int?
|
||||
let lastStudiedAt: String?
|
||||
|
||||
@ -122,6 +122,13 @@ class KnowledgeBaseService {
|
||||
return try await client.request("/knowledge-bases/\(id)", method: "PATCH", body: body)
|
||||
}
|
||||
|
||||
func listSubscribed(page: Int = 1, limit: Int = 20) async throws -> [KnowledgeBase] {
|
||||
return try await client.request("/knowledge-bases/subscribed", queryItems: [
|
||||
URLQueryItem(name: "page", value: String(page)),
|
||||
URLQueryItem(name: "limit", value: String(limit)),
|
||||
])
|
||||
}
|
||||
|
||||
func delete(id: String) async throws -> GenericSuccessResponse {
|
||||
return try await client.request("/knowledge-bases/\(id)", method: "DELETE")
|
||||
}
|
||||
|
||||
@ -16,31 +16,47 @@ struct LibraryHomeView: View {
|
||||
.frame(width: 36, height: 36).background(Color(hex:"#FFFFFF",opacity:0.05))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.zxBorder008, lineWidth: 1))
|
||||
}
|
||||
.accessibilityLabel("搜索知识库")
|
||||
}.accessibilityLabel("搜索知识库")
|
||||
NavigationLink(value: Route.libraryCreate) {
|
||||
Image(systemName: "plus").font(.system(size: 18)).foregroundColor(.white)
|
||||
.frame(width: 36, height: 36).background(ZXGradient.brand)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
.accessibilityLabel("创建新知识库")
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 12)
|
||||
HStack(spacing: 8) { Image(systemName: "magnifyingglass").font(.system(size: 16)).foregroundColor(Color.zxF03); TextField("搜索知识库或知识点…", text: $s).font(.system(size: 14)).tint(Color.zxPurple).accessibilityLabel("搜索知识库") }
|
||||
.padding(.horizontal, 14).frame(height: 44).background(Color.zxFill004).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 14)).padding(.horizontal, 20).padding(.bottom, 16)
|
||||
.accessibilityHint("输入关键词搜索知识库或知识点")
|
||||
.frame(width: 36, height: 36).background(ZXGradient.brand).clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}.accessibilityLabel("创建新知识库")
|
||||
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 12)
|
||||
|
||||
// 搜索
|
||||
HStack(spacing: 8) { Image(systemName: "magnifyingglass").font(.system(size: 16)).foregroundColor(Color.zxF03); TextField("搜索知识库或知识点…", text: $s).font(.system(size: 14)).tint(Color.zxPurple) }
|
||||
.padding(.horizontal, 14).frame(height: 44).background(Color.zxFill004).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 14)).padding(.horizontal, 20).padding(.bottom, 12)
|
||||
|
||||
// 筛选 chips
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(LibraryViewModel.LibraryFilter.allCases, id: \.rawValue) { f in
|
||||
Button {
|
||||
viewModel.currentFilter = f
|
||||
Task { await viewModel.loadKnowledgeBases() }
|
||||
} label: {
|
||||
Text(f.rawValue).font(.system(size: 13, weight: .medium))
|
||||
.foregroundColor(viewModel.currentFilter == f ? Color.zxOnPrimary : Color.zxF05)
|
||||
.padding(.horizontal, 14).padding(.vertical, 7)
|
||||
.background(viewModel.currentFilter == f ? AnyView(ZXGradient.brand) : AnyView(Color.zxFill004))
|
||||
.clipShape(Capsule())
|
||||
.overlay(viewModel.currentFilter == f ? nil : Capsule().stroke(Color.zxBorder008, lineWidth: 1))
|
||||
}
|
||||
}
|
||||
}.padding(.horizontal, 20)
|
||||
}.padding(.bottom, 12)
|
||||
|
||||
ScrollView { VStack(spacing: 12) {
|
||||
if viewModel.isLoading && viewModel.knowledgeBases.isEmpty {
|
||||
VStack(spacing: 12) { ZXLoadingView(size: 36, lineWidth: 3); Text("加载中…").font(.system(size: 13)).foregroundColor(Color.zxF04) }
|
||||
.frame(maxWidth: .infinity).padding(.top, 80)
|
||||
VStack(spacing: 12) { ZXLoadingView(size: 36, lineWidth: 3); Text("加载中…").font(.system(size: 13)).foregroundColor(Color.zxF04) }.frame(maxWidth: .infinity).padding(.top, 80)
|
||||
}
|
||||
ForEach(viewModel.knowledgeBases) { kb in
|
||||
NavigationLink(value: Route.libraryDetail(knowledgeBaseId: kb.id)) {
|
||||
ZLibraryCard(coverUrl: kb.coverUrl, name: kb.title, desc: kb.description ?? "", items: kb.itemCount ?? 0, last: lastStudiedText(kb.lastStudiedAt))
|
||||
ZLibraryCard(coverUrl: kb.coverUrl, name: kb.title, desc: kb.description ?? "", items: kb.itemCount ?? 0, last: lastStudiedText(kb.lastStudiedAt), isPinned: kb.isPinned ?? false, visibility: kb.visibility ?? "private", ownerType: kb.ownerType ?? "user")
|
||||
}
|
||||
}
|
||||
if viewModel.knowledgeBases.isEmpty && !viewModel.isLoading {
|
||||
Text("还没有知识库,点击右上角 + 创建").font(.system(size: 14)).foregroundColor(Color.zxF04).padding(.top, 60)
|
||||
Text(emptyText).font(.system(size: 14)).foregroundColor(Color.zxF04).padding(.top, 60)
|
||||
}
|
||||
if viewModel.hasMore {
|
||||
ZXLoadMoreFooter { await viewModel.loadMore() }
|
||||
@ -54,15 +70,22 @@ struct LibraryHomeView: View {
|
||||
.navigationDestination(for: Route.self) { $0.destination }
|
||||
}
|
||||
|
||||
private var emptyText: String {
|
||||
switch viewModel.currentFilter {
|
||||
case .subscribed: return "还没有订阅任何知识库"
|
||||
case .official: return "暂无官方知识库"
|
||||
default: return "还没有知识库,点击右上角 + 创建"
|
||||
}
|
||||
}
|
||||
|
||||
private func lastStudiedText(_ iso: String?) -> String {
|
||||
guard let iso else { return "未学习" }
|
||||
return iso.prefix(10).description
|
||||
}
|
||||
}
|
||||
struct ZLibraryCard: View { let coverUrl: String?; let name: String; let desc: String; let items: Int; let last: String
|
||||
struct ZLibraryCard: View { let coverUrl: String?; let name: String; let desc: String; let items: Int; let last: String; let isPinned: Bool; let visibility: String; let ownerType: String
|
||||
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) {
|
||||
@ -77,7 +100,11 @@ struct ZLibraryCard: View { let coverUrl: String?; let name: String; let desc: S
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(name).font(.system(size: 16, weight: .bold)).foregroundColor(Color.zxF0)
|
||||
HStack(spacing: 6) {
|
||||
Text(name).font(.system(size: 16, weight: .bold)).foregroundColor(Color.zxF0)
|
||||
if isPinned { Image(systemName: "pin.fill").font(.system(size: 10)).foregroundColor(Color.zxOrange) }
|
||||
if visibility == "public" { Text("公开").font(.system(size: 9, weight: .semibold)).foregroundColor(Color.zxGreen).padding(.horizontal, 5).padding(.vertical, 1).background(Color.zxGreen.opacity(0.12)).clipShape(Capsule()) }
|
||||
}
|
||||
if !desc.isEmpty { Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04).lineLimit(1) }
|
||||
}
|
||||
Spacer()
|
||||
|
||||
@ -13,40 +13,32 @@ class LibraryViewModel: ObservableObject {
|
||||
private var currentPage = 1
|
||||
private let pageSize = 20
|
||||
|
||||
var currentFilter: LibraryFilter = .all
|
||||
|
||||
enum LibraryFilter: String, CaseIterable { case all = "全部", mine = "我的", subscribed = "已订阅", official = "官方" }
|
||||
|
||||
func loadKnowledgeBases() async {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
currentPage = 1
|
||||
isLoading = true; errorMessage = nil; currentPage = 1
|
||||
do {
|
||||
knowledgeBases = try await KnowledgeBaseService.shared.list(page: 1, limit: pageSize)
|
||||
knowledgeBases = try await fetchKBs(page: 1)
|
||||
hasMore = knowledgeBases.count >= pageSize
|
||||
} catch {
|
||||
if knowledgeBases.isEmpty { errorMessage = "加载知识库失败" }
|
||||
}
|
||||
} catch { if knowledgeBases.isEmpty { errorMessage = "加载知识库失败" } }
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
func refresh() async {
|
||||
isRefreshing = true
|
||||
currentPage = 1
|
||||
do {
|
||||
knowledgeBases = try await KnowledgeBaseService.shared.list(page: 1, limit: pageSize)
|
||||
hasMore = knowledgeBases.count >= pageSize
|
||||
} catch {}
|
||||
isRefreshing = true; currentPage = 1
|
||||
do { knowledgeBases = try await fetchKBs(page: 1); hasMore = knowledgeBases.count >= pageSize } catch {}
|
||||
isRefreshing = false
|
||||
}
|
||||
|
||||
func loadMore() async {
|
||||
guard !isLoadingMore, hasMore else { return }
|
||||
isLoadingMore = true
|
||||
currentPage += 1
|
||||
isLoadingMore = true; currentPage += 1
|
||||
do {
|
||||
let more = try await KnowledgeBaseService.shared.list(page: currentPage, limit: pageSize)
|
||||
knowledgeBases.append(contentsOf: more)
|
||||
hasMore = more.count >= pageSize
|
||||
} catch {
|
||||
currentPage -= 1
|
||||
}
|
||||
let more = try await fetchKBs(page: currentPage)
|
||||
knowledgeBases.append(contentsOf: more); hasMore = more.count >= pageSize
|
||||
} catch { currentPage -= 1 }
|
||||
isLoadingMore = false
|
||||
}
|
||||
|
||||
@ -67,8 +59,25 @@ class LibraryViewModel: ObservableObject {
|
||||
_ = try await KnowledgeBaseService.shared.delete(id: id)
|
||||
knowledgeBases.removeAll { $0.id == id }
|
||||
ZXToastManager.shared.success("已删除")
|
||||
} catch {
|
||||
ZXToastManager.shared.error("删除失败")
|
||||
} catch { ZXToastManager.shared.error("删除失败") }
|
||||
}
|
||||
|
||||
func togglePin(id: String) async {
|
||||
do {
|
||||
let _ = try await api.post("/knowledge-bases/\(id)/pin")
|
||||
// reload after pin toggle
|
||||
await loadKnowledgeBases()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
private var api: APIClient { APIClient.shared }
|
||||
|
||||
private func fetchKBs(page: Int) async throws -> [KnowledgeBase] {
|
||||
switch currentFilter {
|
||||
case .all: return try await KnowledgeBaseService.shared.list(page: page, limit: pageSize)
|
||||
case .mine: return try await KnowledgeBaseService.shared.list(page: page, limit: pageSize) // default is user's own
|
||||
case .subscribed: return try await KnowledgeBaseService.shared.listSubscribed(page: page, limit: pageSize)
|
||||
case .official: return [] // TODO: when backend supports ownerType=official
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user