feat: #22 知识库分类管理页面

- KnowledgeFolder 模型 + CreateFolderRequest
- KnowledgeFolderService (list/create/update/delete)
- FolderManageView: 列表 + 新建/重命名/删除
- LibraryDetailPage 菜单「分类管理」入口

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-06 19:26:00 +08:00
parent d9828bc3c8
commit 1f75170b98
4 changed files with 150 additions and 2 deletions

View File

@ -600,6 +600,23 @@ struct BatchAcceptRequest: Codable {
let sourceId: String
}
// MARK: - Knowledge Folder
struct KnowledgeFolder: Codable, Identifiable {
let id: String
let knowledgeBaseId: String?
let parentId: String?
let name: String
let sortOrder: Int?
let createdAt: String?
let updatedAt: String?
}
struct CreateFolderRequest: Codable {
let name: String
let parentId: String?
}
// MARK: - Knowledge Source
struct KnowledgeSource: Codable, Identifiable {

View File

@ -142,6 +142,31 @@ class KnowledgeBaseService {
}
}
// MARK: - Knowledge Folder
@MainActor
class KnowledgeFolderService {
static let shared = KnowledgeFolderService()
private let client = APIClient.shared
func list(kbId: String) async throws -> [KnowledgeFolder] {
return try await client.request("/knowledge-bases/\(kbId)/folders")
}
func create(kbId: String, name: String, parentId: String? = nil) async throws -> KnowledgeFolder {
let body = CreateFolderRequest(name: name, parentId: parentId)
return try await client.request("/knowledge-bases/\(kbId)/folders", method: "POST", body: body)
}
func update(kbId: String, folderId: String, name: String) async throws -> KnowledgeFolder {
return try await client.request("/knowledge-bases/\(kbId)/folders/\(folderId)", method: "PATCH", body: ["name": name])
}
func delete(kbId: String, folderId: String) async throws -> GenericSuccessResponse {
return try await client.request("/knowledge-bases/\(kbId)/folders/\(folderId)", method: "DELETE")
}
}
// MARK: - Knowledge Items
@MainActor

View File

@ -0,0 +1,102 @@
import SwiftUI
struct FolderManageView: View {
let knowledgeBaseId: String
@Environment(\.dismiss) private var dismiss
@State private var folders: [KnowledgeFolder] = []
@State private var isLoading = false
@State private var showCreateAlert = false
@State private var showRenameAlert = false
@State private var newFolderName = ""
@State private var renameFolderId: String?
@State private var renameName = ""
var body: some View {
NavigationStack {
ZStack {
Color.zxBg0.ignoresSafeArea()
if isLoading && folders.isEmpty {
ProgressView().tint(Color.zxPurple)
} else if folders.isEmpty {
Text("暂无分类").font(.system(size: 14)).foregroundColor(Color.zxF04)
} else {
List {
ForEach(folders) { f in
HStack {
Image(systemName: "folder")
.foregroundColor(Color.zxF05)
Text(f.name)
.font(.system(size: 14))
.foregroundColor(Color.zxF0)
Spacer()
}
.padding(.vertical, 4)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
Task {
try? await KnowledgeFolderService.shared.delete(kbId: knowledgeBaseId, folderId: f.id)
folders.removeAll { $0.id == f.id }
}
} label: { Label("删除", systemImage: "trash") }
Button {
renameFolderId = f.id
renameName = f.name
showRenameAlert = true
} label: { Label("重命名", systemImage: "pencil") }
.tint(Color.zxPurple)
}
}
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
}
}
.navigationTitle("分类管理")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
newFolderName = ""
showCreateAlert = true
} label: {
Image(systemName: "plus").font(.system(size: 16)).foregroundColor(Color.zxF05)
}
}
ToolbarItem(placement: .topBarLeading) {
Button("完成") { dismiss() }
.font(.system(size: 14)).foregroundColor(Color.zxF05)
}
}
.alert("新建分类", isPresented: $showCreateAlert) {
TextField("分类名称", text: $newFolderName)
Button("取消", role: .cancel) {}
Button("创建") {
Task {
if let f = try? await KnowledgeFolderService.shared.create(kbId: knowledgeBaseId, name: newFolderName) {
folders.append(f)
}
}
}
}
.alert("重命名", isPresented: $showRenameAlert) {
TextField("新名称", text: $renameName)
Button("取消", role: .cancel) {}
Button("确定") {
if let fid = renameFolderId {
Task {
try? await KnowledgeFolderService.shared.update(kbId: knowledgeBaseId, folderId: fid, name: renameName)
await load()
}
}
}
}
.task { await load() }
}
}
private func load() async {
isLoading = true
folders = (try? await KnowledgeFolderService.shared.list(kbId: knowledgeBaseId)) ?? []
isLoading = false
}
}

View File

@ -143,6 +143,7 @@ struct LibraryDetailPage: View {
@State private var showBatchDeleteConfirm = false
@State private var detailTab = 0
@State private var sortOption = 0
@State private var showFolderManager = false
@State private var sources: [KnowledgeSource] = []
@State private var isLoadingSources = false
@ -340,9 +341,9 @@ struct LibraryDetailPage: View {
Label("添加知识点", image: "icon-plus")
}
Button {
// TODO: create folder
showFolderManager = true
} label: {
Label("创建文件夹", image: "icon-folder")
Label("分类管理", image: "icon-folder")
}
NavigationLink(value: Route.quizList(knowledgeBaseId: knowledgeBaseId)) {
Label("答题测验", image: "icon-pencil")
@ -392,6 +393,9 @@ struct LibraryDetailPage: View {
Text("确定要删除选中的 \(selectedIds.count) 个知识点吗?此操作不可恢复。")
}
.task { await viewModel.loadItems(knowledgeBaseId: knowledgeBaseId) }
.sheet(isPresented: $showFolderManager) {
FolderManageView(knowledgeBaseId: knowledgeBaseId)
}
.onChange(of: sortOption) { _ in
let (sb, od) = sortParams(sortOption)
Task { await viewModel.loadItems(knowledgeBaseId: knowledgeBaseId, sortBy: sb, order: od) }