feat(ios): IOS-M0-04 Knowledge Source 知识源管理
- 新增 KnowledgeSourceService: list/detail/add/delete - 新增 KnowledgeSource/AddSourceRequest 模型 - LibraryDetailPage 新增分段选择器:知识点 | 资料来源 - 资料来源 Tab 显示来源列表(类型图标/标题/解析状态/字数) - 左滑删除资料来源 - 切换 Tab 自动加载数据 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
fea5bca8a6
commit
6664612212
@ -472,6 +472,27 @@ struct BatchAcceptRequest: Codable {
|
|||||||
let sourceId: String
|
let sourceId: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Knowledge Source
|
||||||
|
|
||||||
|
struct KnowledgeSource: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let knowledgeBaseId: String?
|
||||||
|
let title: String?
|
||||||
|
let originalFilename: String?
|
||||||
|
let type: String?
|
||||||
|
let mimeType: String?
|
||||||
|
let parseStatus: String?
|
||||||
|
let indexStatus: String?
|
||||||
|
let textLength: Int?
|
||||||
|
let createdAt: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AddSourceRequest: Codable {
|
||||||
|
let fileId: String?
|
||||||
|
let type: String?
|
||||||
|
let title: String?
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - File Upload (COS presigned URL flow)
|
// MARK: - File Upload (COS presigned URL flow)
|
||||||
|
|
||||||
struct FileUploadUrlRequest: Codable {
|
struct FileUploadUrlRequest: Codable {
|
||||||
|
|||||||
@ -356,6 +356,31 @@ class ImportCandidateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Knowledge Source
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class KnowledgeSourceService {
|
||||||
|
static let shared = KnowledgeSourceService()
|
||||||
|
private let client = APIClient.shared
|
||||||
|
|
||||||
|
func list(kbId: String) async throws -> [KnowledgeSource] {
|
||||||
|
return try await client.request("/knowledge-bases/\(kbId)/sources")
|
||||||
|
}
|
||||||
|
|
||||||
|
func detail(kbId: String, id: String) async throws -> KnowledgeSource {
|
||||||
|
return try await client.request("/knowledge-bases/\(kbId)/sources/\(id)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func add(kbId: String, fileId: String? = nil, type: String? = nil, title: String? = nil) async throws -> KnowledgeSource {
|
||||||
|
let body = AddSourceRequest(fileId: fileId, type: type, title: title)
|
||||||
|
return try await client.request("/knowledge-bases/\(kbId)/sources", method: "POST", body: body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete(kbId: String, id: String) async throws -> GenericSuccessResponse {
|
||||||
|
return try await client.request("/knowledge-bases/\(kbId)/sources/\(id)", method: "DELETE")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Notifications
|
// MARK: - Notifications
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|||||||
@ -140,6 +140,9 @@ struct LibraryDetailPage: View {
|
|||||||
@State private var isSelectMode = false
|
@State private var isSelectMode = false
|
||||||
@State private var selectedIds: Set<String> = []
|
@State private var selectedIds: Set<String> = []
|
||||||
@State private var showBatchDeleteConfirm = false
|
@State private var showBatchDeleteConfirm = false
|
||||||
|
@State private var detailTab = 0
|
||||||
|
@State private var sources: [KnowledgeSource] = []
|
||||||
|
@State private var isLoadingSources = false
|
||||||
|
|
||||||
private var allSelected: Bool {
|
private var allSelected: Bool {
|
||||||
!viewModel.items.isEmpty && selectedIds.count == viewModel.items.count
|
!viewModel.items.isEmpty && selectedIds.count == viewModel.items.count
|
||||||
@ -147,7 +150,15 @@ struct LibraryDetailPage: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) {
|
ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) {
|
||||||
|
Picker("", selection: $detailTab) {
|
||||||
|
Text("知识点").tag(0)
|
||||||
|
Text("资料来源").tag(1)
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
.padding(.horizontal, 20).padding(.top, 8)
|
||||||
|
|
||||||
ScrollView { VStack(spacing: 12) {
|
ScrollView { VStack(spacing: 12) {
|
||||||
|
if detailTab == 0 {
|
||||||
if viewModel.isLoading && viewModel.items.isEmpty {
|
if viewModel.isLoading && viewModel.items.isEmpty {
|
||||||
VStack(spacing: 12) { ZXLoadingView(size: 36, lineWidth: 3); Text("加载中…").font(.system(size: 13)).foregroundColor(Color.zxF04) }
|
VStack(spacing: 12) { ZXLoadingView(size: 36, lineWidth: 3); Text("加载中…").font(.system(size: 13)).foregroundColor(Color.zxF04) }
|
||||||
.frame(maxWidth: .infinity).padding(.top, 80)
|
.frame(maxWidth: .infinity).padding(.top, 80)
|
||||||
@ -180,10 +191,51 @@ struct LibraryDetailPage: View {
|
|||||||
}
|
}
|
||||||
}.padding(.horizontal, 20).padding(.bottom, 80) }
|
}.padding(.horizontal, 20).padding(.bottom, 80) }
|
||||||
.scrollIndicators(.hidden)
|
.scrollIndicators(.hidden)
|
||||||
|
.zxPullToRefresh { await viewModel.refresh(knowledgeBaseId: knowledgeBaseId) }
|
||||||
|
} else {
|
||||||
|
// 资料来源 Tab
|
||||||
|
if isLoadingSources {
|
||||||
|
VStack(spacing: 12) { ProgressView().tint(Color.zxPurple); Text("加载中…").font(.system(size: 13)).foregroundColor(Color.zxF04) }.padding(.top, 80)
|
||||||
|
} else if sources.isEmpty {
|
||||||
|
Text("暂无资料来源").font(.system(size: 13)).foregroundColor(Color.zxF03).padding(.top, 40)
|
||||||
|
} else {
|
||||||
|
ForEach(sources) { src in
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: src.type == "file" ? "doc.fill" : "link")
|
||||||
|
.font(.system(size: 18)).foregroundColor(Color.zxPurple)
|
||||||
|
.frame(width: 40, height: 40).background(Color.zxPurpleBG(0.12)).clipShape(RoundedRectangle(cornerRadius: 10))
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(src.title ?? src.originalFilename ?? "未命名").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0).lineLimit(1)
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Text(src.parseStatus ?? "pending").font(.system(size: 10, weight: .semibold))
|
||||||
|
.foregroundColor(src.parseStatus == "completed" ? Color.zxGreen : Color.zxAmber)
|
||||||
|
.padding(.horizontal, 6).padding(.vertical, 1)
|
||||||
|
.background((src.parseStatus == "completed" ? Color.zxGreen : Color.zxAmber).opacity(0.12)).clipShape(Capsule())
|
||||||
|
if let len = src.textLength, len > 0 { Text("\(len) 字").font(.system(size: 10)).foregroundColor(Color.zxF04) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Button {
|
||||||
|
Task { await deleteSource(src) }
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "trash").font(.system(size: 14)).foregroundColor(Color.zxF03)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(12).background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 14))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.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()
|
.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).animatedTabBarHide()
|
||||||
|
.onChange(of: detailTab) { _, newTab in
|
||||||
|
if newTab == 1 && sources.isEmpty { Task { await loadSources() } }
|
||||||
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
if isSelectMode {
|
if isSelectMode {
|
||||||
ToolbarItem(placement: .topBarLeading) {
|
ToolbarItem(placement: .topBarLeading) {
|
||||||
@ -246,6 +298,20 @@ struct LibraryDetailPage: View {
|
|||||||
}
|
}
|
||||||
.task { await viewModel.loadItems(knowledgeBaseId: knowledgeBaseId) }
|
.task { await viewModel.loadItems(knowledgeBaseId: knowledgeBaseId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func loadSources() async {
|
||||||
|
isLoadingSources = true
|
||||||
|
do { sources = try await KnowledgeSourceService.shared.list(kbId: knowledgeBaseId) } catch {}
|
||||||
|
isLoadingSources = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deleteSource(_ src: KnowledgeSource) async {
|
||||||
|
do {
|
||||||
|
let _ = try await KnowledgeSourceService.shared.delete(kbId: knowledgeBaseId, id: src.id)
|
||||||
|
sources.removeAll { $0.id == src.id }
|
||||||
|
ZXToastManager.shared.success("已删除")
|
||||||
|
} catch { ZXToastManager.shared.error("删除失败") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
struct ZXCardRow: View { let icon: String; let title: String; let desc: String; let status: String; let c: Color
|
struct ZXCardRow: View { let icon: String; let title: String; let desc: String; let status: String; let c: Color
|
||||||
var body: some View { HStack(spacing: 12) { Image(systemName: icon).font(.system(size: 18)).foregroundColor(c).frame(width: 40, height: 40).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 10)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 11)).foregroundColor(Color.zxF03) }; Spacer(); Text(status).font(.system(size: 10, weight: .semibold)).foregroundColor(c).padding(.horizontal, 8).padding(.vertical, 2).background(c.opacity(0.12)).clipShape(Capsule()) }
|
var body: some View { HStack(spacing: 12) { Image(systemName: icon).font(.system(size: 18)).foregroundColor(c).frame(width: 40, height: 40).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 10)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 11)).foregroundColor(Color.zxF03) }; Spacer(); Text(status).font(.system(size: 10, weight: .semibold)).foregroundColor(c).padding(.horizontal, 8).padding(.vertical, 2).background(c.opacity(0.12)).clipShape(Capsule()) }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user