wangdl 32790070aa fix(ios): 删除多余的闭合括号
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 10:44:02 +08:00

570 lines
33 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
import PhotosUI
import UniformTypeIdentifiers
struct CreateLibraryPage: View {
@Environment(\.dismiss) private var dismiss
@State private var name = ""; @State private var desc = ""
@State private var isCreating = false; @State private var isUploadingCover = false
@State private var coverKey: String?; @State private var coverImage: UIImage?
@State private var showCoverPicker = false; @State private var coverPhotoItem: PhotosPickerItem?
var body: some View {
ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) {
ScrollView { VStack(spacing: 20) {
//
VStack(alignment: .leading, spacing: 8) {
Text("封面图可选3:2").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
Button { showCoverPicker = true } label: {
ZStack {
RoundedRectangle(cornerRadius: 14).fill(Color.zxFill004)
.overlay(RoundedRectangle(cornerRadius: 14).stroke(style: StrokeStyle(lineWidth: 1.5, dash: [6, 4])).foregroundColor(Color.zxBorder01))
.frame(height: 160)
if let img = coverImage {
Image(uiImage: img).resizable().scaledToFill().frame(height: 160).clipShape(RoundedRectangle(cornerRadius: 14))
} else {
VStack(spacing: 8) {
Image(systemName: "photo.badge.plus").font(.system(size: 28)).foregroundColor(Color.zxF04)
Text("点击上传封面图").font(.system(size: 13)).foregroundColor(Color.zxF04)
}
}
if isUploadingCover { RoundedRectangle(cornerRadius: 14).fill(Color.black.opacity(0.4)).frame(height: 160); ProgressView().tint(.white) }
}
}.disabled(isUploadingCover)
}
VStack(alignment: .leading, spacing: 8) { Text("知识库名称").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035); TextField("例如:机器学习", text: $name).font(.system(size: 15)).tint(Color.zxPurple).padding(.horizontal, 16).frame(height: 52).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)) }
VStack(alignment: .leading, spacing: 8) { Text("描述(可选)").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035); TextField("简单描述这个知识库的内容", text: $desc).font(.system(size: 15)).tint(Color.zxPurple).padding(.horizontal, 16).frame(height: 52).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)) }
Button {
guard !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
isCreating = true
Task {
let kb = try? await KnowledgeBaseService.shared.create(
title: name, description: desc.isEmpty ? nil : desc, coverKey: coverKey
)
await MainActor.run {
isCreating = false
if kb != nil {
ZXToastManager.shared.success("知识库已创建")
dismiss()
} else {
ZXToastManager.shared.error("创建失败,请重试")
}
}
}
} label: {
HStack(spacing: 8) {
if isCreating { ProgressView().tint(.white) }
Text("创建").font(.system(size: 14, weight: .bold))
}
.foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52)
.background(name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? AnyView(Color.zxHairlineStrong) : AnyView(ZXGradient.ctaPurple))
.clipShape(RoundedRectangle(cornerRadius: 16))
}
.disabled(isCreating || name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}.padding(.horizontal, 20).padding(.top, 20) }.scrollIndicators(.hidden) }
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).animatedTabBarHide()
.photosPicker(isPresented: $showCoverPicker, selection: $coverPhotoItem, matching: .images)
.onChange(of: coverPhotoItem) { _, item in
guard let item else { return }
Task { await uploadCover(item) }
}}
private func uploadCover(_ item: PhotosPickerItem) async {
isUploadingCover = true
defer { isUploadingCover = false; coverPhotoItem = nil }
guard let data = try? await item.loadTransferable(type: Data.self),
let image = UIImage(data: data) else { return }
let resized = resizeCoverImage(image, tw: 600, th: 400)
do {
let r = try await FileUploadService.shared.uploadImageWithKey(resized, filename: "cover_\(Int(Date().timeIntervalSince1970)).jpg")
coverKey = r.objectKey; coverImage = resized
} catch { ZXToastManager.shared.error("封面上传失败") }
}
private func resizeCoverImage(_ image: UIImage, tw: CGFloat, th: CGFloat) -> UIImage {
let tr = tw / th; let ir = image.size.width / image.size.height
let rect: CGRect
if ir > tr { let nw = image.size.height * tr; rect = CGRect(x: (image.size.width - nw)/2, y: 0, width: nw, height: image.size.height) }
else { let nh = image.size.width / tr; rect = CGRect(x: 0, y: (image.size.height - nh)/2, width: image.size.width, height: nh) }
guard let cg = image.cgImage?.cropping(to: rect) else { return image }
let r = UIGraphicsImageRenderer(size: CGSize(width: tw, height: th))
return r.image { _ in UIImage(cgImage: cg).draw(in: CGRect(origin: .zero, size: CGSize(width: tw, height: th))) }
}
}
struct LibraryDetailPage: View {
let knowledgeBaseId: String
@Environment(\.dismiss) private var dismiss
@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) {
ScrollView { VStack(spacing: 12) {
if viewModel.isLoading && viewModel.items.isEmpty {
VStack(spacing: 12) { ZXLoadingView(size: 36, lineWidth: 3); Text("加载中…").font(.system(size: 13)).foregroundColor(Color.zxF04) }
.frame(maxWidth: .infinity).padding(.top, 80)
}
ForEach(viewModel.items) { item in
if isSelectMode {
Button {
if selectedIds.contains(item.id) { selectedIds.remove(item.id) }
else { selectedIds.insert(item.id) }
} label: {
HStack(spacing: 10) {
Image(systemName: selectedIds.contains(item.id) ? "checkmark.circle.fill" : "circle")
.font(.system(size: 20))
.foregroundColor(selectedIds.contains(item.id) ? Color.zxPrimary : Color.zxF03)
ZXCardRow(icon: "doc.text", title: item.title, desc: item.summary ?? item.content ?? "", status: item.status ?? "active", c: Color.zxGreen)
}
}
.foregroundColor(.primary)
} else {
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)
}
}
}
if viewModel.items.isEmpty && !viewModel.isLoading {
Text("暂无知识点").font(.system(size: 13)).foregroundColor(Color.zxF03).padding(.top, 40)
}
if viewModel.hasMore {
ZXLoadMoreFooter { await viewModel.loadMore(knowledgeBaseId: knowledgeBaseId) }
}
}.padding(.horizontal, 20).padding(.bottom, 80) }
.scrollIndicators(.hidden)
.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) {
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: .topBarLeading) {
Button { showDeleteConfirm = true } label: {
Image(systemName: "trash").font(.system(size: 16)).foregroundColor(Color.zxF03)
}
}
ToolbarItem(placement: .topBarTrailing) {
NavigationLink(value: Route.addKnowledge(knowledgeBaseId: knowledgeBaseId)) {
Image(systemName: "plus").font(.system(size: 16, weight: .semibold)).foregroundColor(Color.zxPrimary)
}
}
ToolbarItem(placement: .topBarTrailing) {
Button { isSelectMode = true } label: {
Image(systemName: "checkmark.circle").font(.system(size: 16)).foregroundColor(Color.zxF05)
}
}
}
}
.alert("删除知识库", isPresented: $showDeleteConfirm) {
Button("取消", role: .cancel) {}
Button("删除", role: .destructive) {
isDeleting = true
Task {
await viewModel.deleteKnowledgeBase(id: knowledgeBaseId)
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) }
}
}
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()) }
.padding(14).background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder006, lineWidth: 1)) }
}
struct AddKnowledgePage: View {
let knowledgeBaseId: String
@Environment(\.dismiss) private var dismiss
@State private var title = ""
@State private var content = ""
@State private var sourceType: SourceType = .file
@State private var selectedFiles: [SelectedFile] = []
@State private var isUploading = false
@State private var isSaving = false
@State private var showFilePicker = false
@State private var showPhotoPicker = false
@State private var selectedPhotoItems: [PhotosPickerItem] = []
struct SelectedFile: Identifiable { let id = UUID(); let name: String; let data: Data; let icon: String; let size: String }
enum SourceType: String, CaseIterable { case manual = "手写", file = "文件" }
var body: some View {
ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) {
ScrollView { VStack(spacing: 16) {
//
VStack(alignment: .leading, spacing: 8) {
Text("内容来源").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
Picker("", selection: $sourceType) {
ForEach(SourceType.allCases, id: \.self) { t in Text(t.rawValue).tag(t) }
}
.pickerStyle(.segmented)
}
//
switch sourceType {
case .manual:
VStack(alignment: .leading, spacing: 8) {
Text("标题").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
TextField("输入知识点标题", text: $title).font(.system(size: 15)).tint(Color.zxPurple)
.padding(.horizontal, 16).frame(height: 52)
.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
}
VStack(alignment: .leading, spacing: 8) {
Text("内容").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
TextEditor(text: $content)
.frame(minHeight: 200).scrollContentBackground(.hidden).padding(12)
.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
}
case .file:
VStack(spacing: 10) {
HStack(spacing: 8) {
Button { showFilePicker = true } label: {
HStack {
Image(systemName: "doc.badge.plus")
Text("选择文件")
}
.font(.system(size: 14, weight: .medium))
.foregroundColor(Color.zxPrimary)
.frame(maxWidth: .infinity).frame(height: 48)
.background(Color.zxPrimarySoft)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
Button { showPhotoPicker = true } label: {
HStack {
Image(systemName: "photo.on.rectangle")
Text("图片")
}
.font(.system(size: 14, weight: .medium))
.foregroundColor(Color.zxAccent)
.frame(maxWidth: .infinity).frame(height: 48)
.background(Color.zxAccent.opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
//
if !selectedFiles.isEmpty {
VStack(spacing: 6) {
ForEach(selectedFiles) { f in
HStack(spacing: 8) {
Image(systemName: f.icon).foregroundColor(Color.zxGreen)
Text(f.name).font(.system(size: 13)).foregroundColor(Color.zxF0).lineLimit(1)
Spacer()
Text(f.size).font(.system(size: 11)).foregroundColor(Color.zxF04)
Button {
selectedFiles.removeAll { $0.id == f.id }
} label: {
Image(systemName: "xmark.circle.fill").font(.system(size: 16)).foregroundColor(Color.zxF04)
}
}
.padding(.horizontal, 12).padding(.vertical, 8)
.background(Color.zxFill004)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
}
HStack {
Image(systemName: "info.circle").font(.system(size: 11))
Text("支持多选,每个文件生成一个知识点")
}
.font(.system(size: 11)).foregroundColor(Color.zxF04)
}
if isUploading {
HStack(spacing: 8) {
ProgressView()
Text("上传中 \(currentUploadIndex)/\(selectedFiles.count)...").font(.system(size: 13)).foregroundColor(Color.zxF04)
}
}
}
//
Button {
Task { await save() }
} label: {
HStack(spacing: 8) {
if isSaving { ProgressView().tint(.white) }
Text(sourceType == .file && selectedFiles.count > 1 ? "批量添加 (\(selectedFiles.count) 个)" : "保存").font(.system(size: 14, weight: .bold))
}
.foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52)
.background(canSave ? AnyView(ZXGradient.ctaPurple) : AnyView(Color.zxHairlineStrong))
.clipShape(RoundedRectangle(cornerRadius: 16))
}
.disabled(!canSave || isSaving)
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80) }.scrollIndicators(.hidden) }
}
.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).animatedTabBarHide()
.fileImporter(isPresented: $showFilePicker, allowedContentTypes: [.plainText, .pdf, .image], allowsMultipleSelection: true) { result in
if case .success(let urls) = result { handleFiles(urls) }
}
.photosPicker(isPresented: $showPhotoPicker, selection: $selectedPhotoItems, maxSelectionCount: 10, matching: .images)
.onChange(of: selectedPhotoItems) { _, items in
guard !items.isEmpty else { return }
Task { await handlePhotos(items) }
}
}
// MARK: - File handling
@State private var currentUploadIndex = 0
private var canSave: Bool {
switch sourceType {
case .manual: return !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
case .file: return !selectedFiles.isEmpty && !isUploading
}
}
private func handleFiles(_ urls: [URL]) {
for url in urls {
guard url.startAccessingSecurityScopedResource() else { continue }
defer { url.stopAccessingSecurityScopedResource() }
guard let data = try? Data(contentsOf: url) else { continue }
let name = url.lastPathComponent
selectedFiles.append(SelectedFile(
name: name,
data: data,
icon: fileIcon(name: name),
size: formatSize(data.count)
))
}
}
private func handlePhotos(_ items: [PhotosPickerItem]) async {
for item in items {
guard let data = try? await item.loadTransferable(type: Data.self) else { continue }
let compressed: Data
if let image = UIImage(data: data), data.count > 2 * 1024 * 1024 {
compressed = image.jpegData(compressionQuality: 0.6) ?? data
} else {
compressed = data
}
let name = "image_\(Int(Date().timeIntervalSince1970))_\(selectedFiles.count).jpg"
selectedFiles.append(SelectedFile(
name: name,
data: compressed,
icon: "photo",
size: formatSize(compressed.count)
))
}
selectedPhotoItems = []
}
private func save() async {
isSaving = true
defer { isSaving = false }
switch sourceType {
case .manual:
do {
_ = try await KnowledgeItemService.shared.create(
knowledgeBaseId: knowledgeBaseId,
title: title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "手写笔记" : title,
content: content,
itemType: "manual"
)
ZXToastManager.shared.success("知识点已添加")
dismiss()
} catch {
ZXToastManager.shared.error("添加失败: \(error.localizedDescription)")
}
case .file:
isUploading = true
defer { isUploading = false }
var successCount = 0
let total = selectedFiles.count
for (i, file) in selectedFiles.enumerated() {
currentUploadIndex = i + 1
do {
let mime = mimeType(name: file.name)
let ext = file.name.lowercased()
let itemType = ext.hasSuffix(".md") || ext.hasSuffix(".markdown") ? "markdown"
: ext.hasSuffix(".txt") ? "text"
: ext.hasSuffix(".pdf") ? "pdf"
: ext.hasSuffix(".jpg") || ext.hasSuffix(".jpeg") || ext.hasSuffix(".png") || ext.hasSuffix(".heic") ? "image"
: "file"
let fileId: String
if let image = UIImage(data: file.data) {
fileId = try await FileUploadService.shared.uploadImage(image, filename: file.name)
} else {
fileId = try await FileUploadService.shared.uploadData(file.data, filename: file.name, mimeType: mime)
}
let downloadUrl = try await FileUploadService.shared.getDownloadUrl(fileId: fileId)
//
let itemTitle: String
if !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
itemTitle = total == 1 ? title : "\(title) (\(i+1))"
} else {
itemTitle = file.name
}
_ = try await KnowledgeItemService.shared.create(
knowledgeBaseId: knowledgeBaseId,
title: itemTitle,
content: downloadUrl,
itemType: itemType
)
successCount += 1
} catch {
ZXToastManager.shared.error("\(file.name)」上传失败")
}
}
if successCount == total {
ZXToastManager.shared.success("\(total) 个知识点已添加")
dismiss()
} else if successCount > 0 {
ZXToastManager.shared.success("\(successCount)/\(total) 个知识点已添加")
dismiss()
}
}
}
private func fileIcon(name: String) -> String {
let ext = name.lowercased()
if ext.hasSuffix(".md") || ext.hasSuffix(".markdown") { return "doc.richtext" }
if ext.hasSuffix(".txt") { return "doc.plaintext" }
if ext.hasSuffix(".pdf") { return "doc.fill" }
if ext.hasSuffix(".jpg") || ext.hasSuffix(".jpeg") || ext.hasSuffix(".png") || ext.hasSuffix(".heic") { return "photo" }
return "doc"
}
private func formatSize(_ size: Int) -> String {
if size < 1024 { return "\(size) B" }
if size < 1024 * 1024 { return "\(size / 1024) KB" }
return String(format: "%.1f MB", Double(size) / 1024 / 1024)
}
private func mimeType(name: String) -> String {
let ext = name.lowercased()
if ext.hasSuffix(".md") || ext.hasSuffix(".markdown") { return "text/markdown" }
if ext.hasSuffix(".txt") { return "text/plain" }
if ext.hasSuffix(".pdf") { return "application/pdf" }
if ext.hasSuffix(".jpg") || ext.hasSuffix(".jpeg") { return "image/jpeg" }
if ext.hasSuffix(".png") { return "image/png" }
if ext.hasSuffix(".heic") { return "image/heic" }
return "application/octet-stream"
}
}
struct KnowledgeDetailPage: View {
let item: KnowledgeItem
var body: some View {
ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) {
HStack { Spacer()
NavigationLink(value: Route.editKnowledge(item: item)) {
Image(systemName: "pencil").font(.system(size: 16)).foregroundColor(Color.zxF05)
.frame(width: 36, height: 36).background(Color(hex:"#FFFFFF",opacity:0.05))
.clipShape(RoundedRectangle(cornerRadius: 10))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.zxBorder008, lineWidth: 1))
}
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 8)
ScrollView { VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
HStack {
if let itemType = item.itemType { ZXChip(text: itemType, color: Color.zxPurple) }
if let sourceType = item.sourceType { ZXChip(text: sourceType, color: Color.zxAccent) }
}
Text(item.title).font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxF0)
if let content = item.content { Text(content).font(.system(size: 14)).foregroundColor(Color.zxF007).lineSpacing(6) }
}.padding(20).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
HStack(spacing: 12) {
NavigationLink(value: Route.studyHome) {
Label("开始复习", systemImage: "arrow.triangle.2.circlepath").font(.system(size: 14, weight: .bold)).foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 44).background(ZXGradient.brandPurple).clipShape(RoundedRectangle(cornerRadius: 14))
}
NavigationLink(value: Route.aiChat) {
Label("费曼解释", systemImage: "mic.fill").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF05).frame(maxWidth: .infinity).frame(height: 44).background(Color.zxFill005).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
}
}
}.padding(.horizontal, 20).padding(.bottom, 80) }.scrollIndicators(.hidden) }
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).animatedTabBarHide()}
}
struct ZXChip: View { let text: String; let color: Color
var body: some View { Text(text).font(.system(size: 10, weight: .semibold)).foregroundColor(color).padding(.horizontal, 8).padding(.vertical, 2).background(color.opacity(0.12)).clipShape(Capsule()) }
}
struct ImportPage: View {
var body: some View {
ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) {
ScrollView { VStack(spacing: 12) {
ZXImportOption(icon: "camera.fill", title: "拍照导入", desc: "拍下书本或笔记AI 自动识别")
ZXImportOption(icon: "doc.text.fill", title: "文件导入", desc: "支持 PDF、Word、Markdown")
ZXImportOption(icon: "link", title: "链接导入", desc: "粘贴网页链接,自动提取内容")
ZXImportOption(icon: "photo.on.rectangle", title: "相册导入", desc: "从相册选择截图或图片")
}.padding(.horizontal, 20).padding(.top, 8) }.scrollIndicators(.hidden) }
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).animatedTabBarHide()}
}
struct ZXImportOption: View { let icon: String; let title: String; let desc: String
var body: some View { Button { } label: { HStack(spacing: 14) { Image(systemName: icon).font(.system(size: 22)).foregroundColor(Color.zxPurple).frame(width: 48, height: 48).background(Color.zxPurpleBG(0.1)).clipShape(RoundedRectangle(cornerRadius: 14)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 15, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04) }; Spacer(); Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03) }.padding(16).background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 16)).overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)) }.foregroundColor(.primary) }
}
struct EditKnowledgePage: View {
let item: KnowledgeItem
@State private var title: String; @State private var content: String
init(item: KnowledgeItem) {
self.item = item
_title = State(initialValue: item.title)
_content = State(initialValue: item.content ?? "")
}
var body: some View {
ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) {
ScrollView { VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 8) { Text("标题").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035); TextField("", text: $title).font(.system(size: 15)).tint(Color.zxPurple).padding(.horizontal, 16).frame(height: 52).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)) }
VStack(alignment: .leading, spacing: 8) { Text("内容").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035); TextEditor(text: $content).frame(minHeight: 200).scrollContentBackground(.hidden).padding(12).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)) }
Button {
Task { _ = try? await KnowledgeItemService.shared.update(id: item.id, title: title, content: content, summary: nil) }
} label: { Text("保存修改").font(.system(size: 14, weight: .bold)).foregroundColor(.white).frame(maxWidth: .infinity).frame(height: 52).background(ZXGradient.ctaPurple).clipShape(RoundedRectangle(cornerRadius: 16)) }
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80) }.scrollIndicators(.hidden) }
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).animatedTabBarHide()}
}