feat(ios): 知识点删除 + 批量选择删除

- 新增单个删除 + 批量删除 API 调用
- LibraryDetailPage 新增选择模式:
  - ⋯ 菜单 → "选择知识点" 进入多选模式
  - 全选/取消全选 + 批量删除按钮
  - 选中的 items 显示选中标记
  - 选择模式下禁止导航进入详情
- LibraryDetailViewModel 新增 deleteItem + batchDeleteItems

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-05-27 22:47:24 +08:00
parent ed3e587bf0
commit e1bfda0169
4 changed files with 104 additions and 18 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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<String> = []
@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,9 +61,22 @@ struct LibraryDetailPage: View {
.frame(maxWidth: .infinity).padding(.top, 80)
}
ForEach(viewModel.items) { item in
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 {
Text("暂无知识点").font(.system(size: 13)).foregroundColor(Color.zxF03).padding(.top, 40)
@ -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 {
if isSelectMode {
ToolbarItem(placement: .topBarLeading) {
Button("取消") { isSelectMode = false; selectedIds.removeAll() }
}
ToolbarItem(placement: .topBarTrailing) {
NavigationLink(value: Route.addKnowledge(knowledgeBaseId: knowledgeBaseId)) {
Image(systemName: "plus").font(.system(size: 16, weight: .semibold))
.foregroundColor(Color.zxPrimary)
Button { selectedIds = allSelected ? [] : Set(viewModel.items.map(\.id)) } label: {
Text(allSelected ? "取消全选" : "全选").font(.system(size: 14))
}
}
ToolbarItem(placement: .topBarTrailing) {
Button {
showDeleteConfirm = true
showBatchDeleteConfirm = true
} label: {
Image(systemName: "trash").font(.system(size: 16))
.foregroundColor(Color.zxCoral)
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) }
}
}

View File

@ -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("批量删除失败")
}
}
}