diff --git a/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift b/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift index 4c8b58c..9b17be8 100644 --- a/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift +++ b/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift @@ -379,6 +379,12 @@ struct GenericSuccessResponse: Codable { let message: String? } +struct BatchDeleteResponse: Codable { + let success: Bool? + let message: String? + let count: Int? +} + // MARK: - File Upload (COS presigned URL flow) struct FileUploadUrlRequest: Codable { diff --git a/AIStudyApp/AIStudyApp/Core/Services/APIService.swift b/AIStudyApp/AIStudyApp/Core/Services/APIService.swift index 86e43a6..0a9d012 100644 --- a/AIStudyApp/AIStudyApp/Core/Services/APIService.swift +++ b/AIStudyApp/AIStudyApp/Core/Services/APIService.swift @@ -135,6 +135,14 @@ class KnowledgeItemService { let body = UpdateKnowledgeItemRequest(title: title, content: content, summary: summary) return try await client.request("/knowledge-items/\(id)", method: "PATCH", body: body) } + + func delete(id: String) async throws { + let _: GenericSuccessResponse = try await client.request("/knowledge-items/\(id)", method: "DELETE") + } + + func batchDelete(ids: [String]) async throws -> BatchDeleteResponse { + return try await client.request("/knowledge-items/batch-delete", method: "POST", body: ["ids": ids]) + } } // MARK: - Active Recall diff --git a/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift b/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift index 7d86e3d..c5e7056 100644 --- a/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift +++ b/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift @@ -45,6 +45,13 @@ struct LibraryDetailPage: View { @StateObject private var viewModel = LibraryDetailViewModel() @State private var showDeleteConfirm = false @State private var isDeleting = false + @State private var isSelectMode = false + @State private var selectedIds: Set = [] + @State private var showBatchDeleteConfirm = false + + private var allSelected: Bool { + !viewModel.items.isEmpty && selectedIds.count == viewModel.items.count + } var body: some View { ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) { @@ -54,8 +61,21 @@ struct LibraryDetailPage: View { .frame(maxWidth: .infinity).padding(.top, 80) } ForEach(viewModel.items) { item in - NavigationLink(value: Route.knowledgeDetail(item: item)) { - ZXCardRow(icon: "doc.text", title: item.title, desc: item.summary ?? item.content ?? "", status: item.status ?? "active", c: Color.zxGreen) + HStack(spacing: 10) { + if isSelectMode { + Button { + if selectedIds.contains(item.id) { selectedIds.remove(item.id) } + else { selectedIds.insert(item.id) } + } label: { + Image(systemName: selectedIds.contains(item.id) ? "checkmark.circle.fill" : "circle") + .font(.system(size: 20)) + .foregroundColor(selectedIds.contains(item.id) ? Color.zxPrimary : Color.zxF03) + } + } + NavigationLink(value: Route.knowledgeDetail(item: item)) { + ZXCardRow(icon: "doc.text", title: item.title, desc: item.summary ?? item.content ?? "", status: item.status ?? "active", c: Color.zxGreen) + } + .disabled(isSelectMode) } } if viewModel.items.isEmpty && !viewModel.isLoading { @@ -66,22 +86,45 @@ struct LibraryDetailPage: View { } }.padding(.horizontal, 20).padding(.bottom, 80) } .scrollIndicators(.hidden) - .zxPullToRefresh { await viewModel.refresh(knowledgeBaseId: knowledgeBaseId) } } + .zxPullToRefresh { await viewModel.refresh(knowledgeBaseId: knowledgeBaseId) } + } } .navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).animatedTabBarHide() .toolbar { - ToolbarItem(placement: .topBarTrailing) { - NavigationLink(value: Route.addKnowledge(knowledgeBaseId: knowledgeBaseId)) { - Image(systemName: "plus").font(.system(size: 16, weight: .semibold)) - .foregroundColor(Color.zxPrimary) + if isSelectMode { + ToolbarItem(placement: .topBarLeading) { + Button("取消") { isSelectMode = false; selectedIds.removeAll() } } - } - ToolbarItem(placement: .topBarTrailing) { - Button { - showDeleteConfirm = true - } label: { - Image(systemName: "trash").font(.system(size: 16)) - .foregroundColor(Color.zxCoral) + ToolbarItem(placement: .topBarTrailing) { + Button { selectedIds = allSelected ? [] : Set(viewModel.items.map(\.id)) } label: { + Text(allSelected ? "取消全选" : "全选").font(.system(size: 14)) + } + } + ToolbarItem(placement: .topBarTrailing) { + Button { + showBatchDeleteConfirm = true + } label: { + Image(systemName: "trash").font(.system(size: 16)).foregroundColor(selectedIds.isEmpty ? Color.zxF03 : Color.zxCoral) + } + .disabled(selectedIds.isEmpty) + } + } else { + ToolbarItem(placement: .topBarTrailing) { + NavigationLink(value: Route.addKnowledge(knowledgeBaseId: knowledgeBaseId)) { + Image(systemName: "plus").font(.system(size: 16, weight: .semibold)).foregroundColor(Color.zxPrimary) + } + } + ToolbarItem(placement: .topBarTrailing) { + Menu { + Button { isSelectMode = true } label: { + Label("选择知识点", systemImage: "checkmark.circle") + } + Button(role: .destructive) { showDeleteConfirm = true } label: { + Label("删除知识库", systemImage: "trash") + } + } label: { + Image(systemName: "ellipsis.circle").font(.system(size: 16)).foregroundColor(Color.zxF05) + } } } } @@ -91,15 +134,24 @@ struct LibraryDetailPage: View { isDeleting = true Task { await viewModel.deleteKnowledgeBase(id: knowledgeBaseId) - await MainActor.run { - isDeleting = false - dismiss() - } + await MainActor.run { isDeleting = false; dismiss() } } } } message: { Text("删除后将无法恢复,包括其中的所有知识点。确定要删除吗?") } + .alert("批量删除", isPresented: $showBatchDeleteConfirm) { + Button("取消", role: .cancel) {} + Button("删除 \(selectedIds.count) 个", role: .destructive) { + Task { + await viewModel.batchDeleteItems(ids: Array(selectedIds)) + isSelectMode = false + selectedIds.removeAll() + } + } + } message: { + Text("确定要删除选中的 \(selectedIds.count) 个知识点吗?此操作不可恢复。") + } .task { await viewModel.loadItems(knowledgeBaseId: knowledgeBaseId) } } } diff --git a/AIStudyApp/AIStudyApp/Features/Library/LibraryViewModel.swift b/AIStudyApp/AIStudyApp/Features/Library/LibraryViewModel.swift index 52ff974..910efb6 100644 --- a/AIStudyApp/AIStudyApp/Features/Library/LibraryViewModel.swift +++ b/AIStudyApp/AIStudyApp/Features/Library/LibraryViewModel.swift @@ -151,4 +151,24 @@ class LibraryDetailViewModel: ObservableObject { return nil } } + + func deleteItem(id: String) async { + do { + try await KnowledgeItemService.shared.delete(id: id) + items.removeAll { $0.id == id } + ZXToastManager.shared.success("知识点已删除") + } catch { + ZXToastManager.shared.error("删除失败") + } + } + + func batchDeleteItems(ids: [String]) async { + do { + let resp = try await KnowledgeItemService.shared.batchDelete(ids: ids) + items.removeAll { ids.contains($0.id) } + ZXToastManager.shared.success(resp.message ?? "已删除") + } catch { + ZXToastManager.shared.error("批量删除失败") + } + } }