From e1bfda01691dd1f670181e1f86804d6b9f0fa382 Mon Sep 17 00:00:00 2001 From: wangdl Date: Wed, 27 May 2026 22:47:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(ios):=20=E7=9F=A5=E8=AF=86=E7=82=B9?= =?UTF-8?q?=E5=88=A0=E9=99=A4=20+=20=E6=89=B9=E9=87=8F=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增单个删除 + 批量删除 API 调用 - LibraryDetailPage 新增选择模式: - ⋯ 菜单 → "选择知识点" 进入多选模式 - 全选/取消全选 + 批量删除按钮 - 选中的 items 显示选中标记 - 选择模式下禁止导航进入详情 - LibraryDetailViewModel 新增 deleteItem + batchDeleteItems Co-Authored-By: Claude Opus 4.7 --- .../AIStudyApp/Core/Models/APIModels.swift | 6 ++ .../AIStudyApp/Core/Services/APIService.swift | 8 ++ .../Features/Library/LibrarySubpages.swift | 88 +++++++++++++++---- .../Features/Library/LibraryViewModel.swift | 20 +++++ 4 files changed, 104 insertions(+), 18 deletions(-) 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("批量删除失败") + } + } }