feat(ios): 创建知识库支持上传封面图
- CreateLibraryPage 新增封面图区域(3:2,600×400) - 自动裁剪 + 缩放到规范尺寸 - 上传到 COS,coverKey 传给后端 - FileUploadService 新增 uploadImageWithKey 返回 fileId+objectKey Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
c021370a82
commit
cc40d8b364
@ -174,6 +174,12 @@ struct KnowledgeBase: Codable, Identifiable {
|
||||
struct CreateKnowledgeBaseRequest: Codable {
|
||||
let title: String
|
||||
let description: String?
|
||||
let coverKey: String?
|
||||
}
|
||||
|
||||
struct KnowledgeBaseCoverUploadResult {
|
||||
let fileId: String
|
||||
let objectKey: String
|
||||
}
|
||||
|
||||
// MARK: - Knowledge Items (matches Prisma KnowledgeItem model)
|
||||
|
||||
@ -85,8 +85,8 @@ class KnowledgeBaseService {
|
||||
])
|
||||
}
|
||||
|
||||
func create(title: String, description: String?) async throws -> KnowledgeBase {
|
||||
let body = CreateKnowledgeBaseRequest(title: title, description: description)
|
||||
func create(title: String, description: String?, coverKey: String? = nil) async throws -> KnowledgeBase {
|
||||
let body = CreateKnowledgeBaseRequest(title: title, description: description, coverKey: coverKey)
|
||||
return try await client.request("/knowledge-bases", method: "POST", body: body)
|
||||
}
|
||||
|
||||
|
||||
@ -6,12 +6,44 @@ class FileUploadService {
|
||||
static let shared = FileUploadService()
|
||||
private let client = APIClient.shared
|
||||
|
||||
/// 上传图片到 COS 并返回文件 ID
|
||||
func uploadImage(_ image: UIImage, filename: String = "avatar.jpg") async throws -> String {
|
||||
guard let imageData = image.jpegData(compressionQuality: 0.7) else {
|
||||
/// 上传图片到 COS 并返回文件 ID + objectKey
|
||||
func uploadImageWithKey(_ image: UIImage, filename: String = "cover.jpg") async throws -> (fileId: String, objectKey: String) {
|
||||
guard let imageData = image.jpegData(compressionQuality: 0.75) else {
|
||||
throw FileUploadError.compressFailed
|
||||
}
|
||||
return try await uploadData(imageData, filename: filename, mimeType: "image/jpeg")
|
||||
return try await uploadDataWithKey(imageData, filename: filename, mimeType: "image/jpeg")
|
||||
}
|
||||
|
||||
/// 上传图片到 COS 并返回文件 ID
|
||||
func uploadImage(_ image: UIImage, filename: String = "avatar.jpg") async throws -> String {
|
||||
let result = try await uploadImageWithKey(image, filename: filename)
|
||||
return result.fileId
|
||||
}
|
||||
|
||||
/// 上传原始数据到 COS 并返回 fileId + objectKey
|
||||
func uploadDataWithKey(_ data: Data, filename: String, mimeType: String) async throws -> (fileId: String, objectKey: String) {
|
||||
let urlReq = FileUploadUrlRequest(
|
||||
filename: filename, mimeType: mimeType, sizeBytes: data.count
|
||||
)
|
||||
let urlResp: FileUploadUrlResponse = try await client.request(
|
||||
"/files/upload-url", method: "POST", body: urlReq
|
||||
)
|
||||
|
||||
var request = URLRequest(url: URL(string: urlResp.uploadUrl)!)
|
||||
request.httpMethod = "PUT"
|
||||
request.setValue(mimeType, forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = data
|
||||
let (_, response) = try await URLSession.shared.data(for: request)
|
||||
guard let httpResp = response as? HTTPURLResponse, httpResp.statusCode == 200 else {
|
||||
throw FileUploadError.uploadFailed
|
||||
}
|
||||
|
||||
let confirmResp: FileConfirmUploadResponse = try await client.request(
|
||||
"/files/complete", method: "POST",
|
||||
body: FileConfirmUploadRequest(objectKey: urlResp.objectKey, checksum: nil)
|
||||
)
|
||||
|
||||
return (fileId: confirmResp.id, objectKey: urlResp.objectKey)
|
||||
}
|
||||
|
||||
/// 上传原始数据到 COS 并返回文件 ID
|
||||
|
||||
@ -4,17 +4,42 @@ 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 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)
|
||||
let kb = try? await KnowledgeBaseService.shared.create(
|
||||
title: name, description: desc.isEmpty ? nil : desc, coverKey: coverKey
|
||||
)
|
||||
await MainActor.run {
|
||||
isCreating = false
|
||||
if kb != nil {
|
||||
@ -36,7 +61,35 @@ struct CreateLibraryPage: View {
|
||||
}
|
||||
.disabled(isCreating || name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||
}.padding(.horizontal, 20).padding(.top, 20) }.scrollIndicators(.hidden) }
|
||||
}.navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).animatedTabBarHide()}
|
||||
}.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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user