wangdl 4ebb70c036 feat: 图标线型化 + 首页重设计 + 知识库卡片优化 + 知识点列表重构
- 所有 SF Symbol .fill 图标替换为线性版本
- 自定义加载动画全部替换为原生 ProgressView/refreshable
- StudyHomeView 重设计:优先级驱动主行动卡片
- ZLibraryCard 重新设计:封面图自适应、信息布局优化
- LibraryDetailPage:顶部KB信息区、···菜单、排序、长按操作
- 知识点列表:文件类型图标、学习时长、分割线样式
- 弥散渐变顶部背景
- 新增 icon-folder、icon-xmark SVG

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 20:07:15 +08:00

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)
}
}