- 所有 SF Symbol .fill 图标替换为线性版本 - 自定义加载动画全部替换为原生 ProgressView/refreshable - StudyHomeView 重设计:优先级驱动主行动卡片 - ZLibraryCard 重新设计:封面图自适应、信息布局优化 - LibraryDetailPage:顶部KB信息区、···菜单、排序、长按操作 - 知识点列表:文件类型图标、学习时长、分割线样式 - 弥散渐变顶部背景 - 新增 icon-folder、icon-xmark SVG Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
201 lines
9.7 KiB
Swift
201 lines
9.7 KiB
Swift
//
|
|
// LibraryHomeView.swift - Page 7
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct LibraryHomeView: View {
|
|
@StateObject private var viewModel = LibraryViewModel()
|
|
@State private var s = ""
|
|
var body: some View {
|
|
ZStack { ZXGradient.page.ignoresSafeArea()
|
|
VStack(spacing: 0) {
|
|
HStack { Text("知识库").font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxF0).tracking(-0.5); Spacer()
|
|
NavigationLink(value: Route.librarySearch) {
|
|
Image("icon-search").resizable().scaledToFit().frame(width: 18, height: 18).foregroundColor(Color.zxF05)
|
|
.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("搜索知识库")
|
|
NavigationLink(value: Route.libraryCreate) {
|
|
Image("icon-plus").resizable().scaledToFit().frame(width: 18, height: 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("icon-search").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: 14, 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) { ProgressView(); Text("加载中…").font(.system(size: 14)).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 ?? "", updatedAt: lastStudiedText(kb.updatedAt), isPinned: kb.isPinned ?? false, visibility: kb.visibility ?? "private", ownerType: kb.ownerType ?? "user")
|
|
}
|
|
}
|
|
if viewModel.knowledgeBases.isEmpty && !viewModel.isLoading {
|
|
Text(emptyText).font(.system(size: 14)).foregroundColor(Color.zxF04).padding(.top, 60)
|
|
}
|
|
if viewModel.hasMore {
|
|
ZXLoadMoreFooter { await viewModel.loadMore() }
|
|
}
|
|
}.padding(.horizontal, 20).padding(.bottom, 120) }
|
|
.scrollIndicators(.hidden)
|
|
.refreshable { await viewModel.refresh() }
|
|
}
|
|
}
|
|
.task { await viewModel.loadKnowledgeBases() }
|
|
.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 updatedAt: String; let isPinned: Bool; let visibility: String; let ownerType: String
|
|
|
|
private var displayDesc: String {
|
|
let d = desc.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if d.isEmpty { return "暂无简介" }
|
|
return d
|
|
}
|
|
|
|
private var sourceLabel: String {
|
|
ownerType == "official" ? "官方" : "个人"
|
|
}
|
|
|
|
var body: some View {
|
|
HStack(alignment: .top, spacing: 14) {
|
|
// Cover image
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 14)
|
|
.fill(Color.zxPurpleBG(0.12))
|
|
.frame(width: 90, height: 90)
|
|
if let url = coverUrl, let imageUrl = resolvedURL(url) {
|
|
AsyncImage(url: imageUrl) { phase in
|
|
switch phase {
|
|
case .success(let img):
|
|
img.resizable().scaledToFill()
|
|
.frame(width: 90, height: 90)
|
|
.clipShape(RoundedRectangle(cornerRadius: 14))
|
|
default:
|
|
fallbackIcon
|
|
}
|
|
}
|
|
} else {
|
|
fallbackIcon
|
|
}
|
|
}
|
|
|
|
// Content
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
HStack(spacing: 6) {
|
|
Text(name)
|
|
.font(.system(size: 18, weight: .semibold))
|
|
.foregroundColor(Color.zxF0)
|
|
.lineLimit(1)
|
|
Spacer()
|
|
if isPinned {
|
|
Image("icon-pin")
|
|
.font(.system(size: 12))
|
|
.foregroundColor(Color.zxOrange)
|
|
}
|
|
if visibility == "public" {
|
|
Text("公开")
|
|
.font(.system(size: 10, weight: .semibold))
|
|
.foregroundColor(Color.zxGreen)
|
|
.padding(.horizontal, 6).padding(.vertical, 2)
|
|
.background(Color.zxGreen.opacity(0.12))
|
|
.clipShape(Capsule())
|
|
}
|
|
}
|
|
|
|
Text(displayDesc)
|
|
.font(.system(size: 14))
|
|
.foregroundColor(Color.zxF04)
|
|
.lineLimit(2, reservesSpace: true)
|
|
|
|
Spacer().frame(height: 4)
|
|
|
|
HStack {
|
|
Text(updatedAt)
|
|
.font(.system(size: 14))
|
|
.foregroundColor(Color.zxF03)
|
|
Spacer()
|
|
Text(sourceLabel)
|
|
.font(.system(size: 14))
|
|
.foregroundColor(Color.zxF03)
|
|
}
|
|
}
|
|
}
|
|
.padding(16)
|
|
.background(Color.zxSurfaceElevated)
|
|
.clipShape(RoundedRectangle(cornerRadius: ZXRadius.xl))
|
|
.overlay(RoundedRectangle(cornerRadius: ZXRadius.xl).stroke(Color.zxHairline, lineWidth: 0.5))
|
|
}
|
|
|
|
private var fallbackIcon: some View {
|
|
Image("icon-books")
|
|
.font(.system(size: 26))
|
|
.foregroundColor(Color.zxPurple.opacity(0.4))
|
|
}
|
|
|
|
private func resolvedURL(_ urlString: String) -> URL? {
|
|
if urlString.hasPrefix("http") { return URL(string: urlString) }
|
|
return URL(string: "https://longde.cloud\(urlString)")
|
|
}
|
|
}
|
|
|
|
struct LibrarySearchView: View {
|
|
@State private var query = ""
|
|
var body: some View {
|
|
ZStack { Color.zxBg0.ignoresSafeArea()
|
|
VStack(spacing: 0) {
|
|
HStack(spacing: 8) { Image("icon-search").font(.system(size: 16)).foregroundColor(Color.zxF03); TextField("搜索知识库或知识点…", text: $query).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(.top, 8).padding(.bottom, 16)
|
|
ScrollView { VStack(spacing: 12) {
|
|
if query.isEmpty {
|
|
VStack(spacing: 12) {
|
|
Image("icon-search").font(.system(size: 36)).foregroundColor(Color.zxF03)
|
|
Text("搜索知识点、知识库或标签").font(.system(size: 14)).foregroundColor(Color.zxF03)
|
|
}.padding(.top, 80)
|
|
}
|
|
}.padding(.horizontal, 20) }.scrollIndicators(.hidden)
|
|
}
|
|
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar)
|
|
}
|
|
}
|