diff --git a/MyQrCode/Views/CodeTypeSelectionView.swift b/MyQrCode/Views/CodeTypeSelectionView.swift index b473533..80b25b1 100644 --- a/MyQrCode/Views/CodeTypeSelectionView.swift +++ b/MyQrCode/Views/CodeTypeSelectionView.swift @@ -99,11 +99,13 @@ struct CodeTypeSelectionView: View { // 下一步按钮 NavigationLink( - destination: CreateCodeView( - selectedDataType: selectedDataType, - selectedBarcodeType: selectedBarcodeType, - selectedQRCodeType: selectedQRCodeType - ) + destination: selectedDataType == .qrcode ? + AnyView(CreateQRCodeView(selectedQRCodeType: selectedQRCodeType)) : + AnyView(CreateCodeView( + selectedDataType: selectedDataType, + selectedBarcodeType: selectedBarcodeType, + selectedQRCodeType: selectedQRCodeType + )) ) { HStack(spacing: 8) { Text("下一步") diff --git a/MyQrCode/Views/CreateQRCodeView.swift b/MyQrCode/Views/CreateQRCodeView.swift new file mode 100644 index 0000000..8ba7c72 --- /dev/null +++ b/MyQrCode/Views/CreateQRCodeView.swift @@ -0,0 +1,438 @@ +import SwiftUI +import CoreData +import CoreImage + +// MARK: - 二维码创建界面 +struct CreateQRCodeView: View { + @Environment(\.dismiss) private var dismiss + @StateObject private var coreDataManager = CoreDataManager.shared + + // 从类型选择界面传入的参数 + let selectedQRCodeType: QRCodeType + + @State private var content = "" + @State private var showingAlert = false + @State private var alertMessage = "" + + // 输入焦点,确保进入页面自动弹出键盘 + @FocusState private var isContentFieldFocused: Bool + + var body: some View { + VStack(spacing: 0) { + inputAndPreviewSection + } + .navigationTitle(selectedQRCodeType.displayName) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("创建") { createQRCode() } + .disabled(content.isEmpty) + .font(.system(size: 16, weight: .semibold)) + } + } + .alert("提示", isPresented: $showingAlert) { + Button("确定") { } + } message: { Text(alertMessage) } + .onAppear { + // 稍延迟以确保进入页面时自动聚焦 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + isContentFieldFocused = true + } + } + .onTapGesture { + // 点击外部关闭键盘 + isContentFieldFocused = false + } + } + + // MARK: - UI Components + + + + private var inputAndPreviewSection: some View { + ScrollView { + VStack(spacing: 24) { + // 输入提示 + VStack(spacing: 12) { + HStack { + Image(systemName: "info.circle") + .font(.caption) + .foregroundColor(.blue) + + Text(getContentHint()) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(nil) + + Spacer() + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color.blue.opacity(0.1)) + ) + } + .padding(.horizontal, 20) + + // 内容输入区域 + VStack(spacing: 16) { + HStack { + Text("输入内容") + .font(.headline) + .foregroundColor(.primary) + + Spacer() + } + + // 多行输入框 + VStack(spacing: 8) { + ZStack { + // 输入框主体 + TextEditor(text: $content) + .frame(minHeight: 120) + .padding(8) + .background(Color(.systemBackground)) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(isContentFieldFocused ? Color.blue : Color(.systemGray4), lineWidth: 1) + ) + .focused($isContentFieldFocused) + .onChange(of: content) { newValue in + // 限制最大字符数为150 + if newValue.count > 150 { + content = String(newValue.prefix(150)) + } + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("完成") { + isContentFieldFocused = false + } + .foregroundColor(.blue) + .font(.system(size: 16, weight: .medium)) + } + } + + // 占位符文本 - 左上角对齐 + if content.isEmpty && !isContentFieldFocused { + VStack { + HStack { + Text(getPlaceholderText()) + .foregroundColor(.secondary) + .font(.body) + Spacer() + } + Spacer() + } + .padding(16) + .allowsHitTesting(false) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } + } + + // 字符计数和限制提示 - 输入框底下 + HStack { + Spacer() + + VStack(alignment: .trailing, spacing: 4) { + // 字符限制提示 + if content.count >= 150 { + HStack(spacing: 4) { + Image(systemName: "exclamationmark.triangle") + .font(.caption) + .foregroundColor(.orange) + + Text("已达到最大字符数") + .font(.caption) + .foregroundColor(.orange) + } + } else if content.count >= 140 { + HStack(spacing: 4) { + Image(systemName: "info.circle") + .font(.caption) + .foregroundColor(.blue) + + Text("接近字符限制") + .font(.caption) + .foregroundColor(.blue) + } + } + + // 字符计数 + Text("\(content.count)/150") + .font(.caption) + .foregroundColor(getCharacterCountColor()) + } + } + } + } + .padding(.horizontal, 20) + + // 预览区域 + if !content.isEmpty { + VStack(spacing: 16) { + HStack { + Text("预览") + .font(.headline) + .foregroundColor(.primary) + + Spacer() + + Button(action: { + // 可以添加分享功能 + }) { + Image(systemName: "square.and.arrow.up") + .font(.system(size: 16)) + .foregroundColor(.blue) + } + } + + VStack(spacing: 16) { + // 二维码图片 + if let qrCodeImage = generateQRCodeImage() { + Image(uiImage: qrCodeImage) + .interpolation(.none) + .resizable() + .scaledToFit() + .frame(width: 200, height: 200) + .background(Color.white) + .cornerRadius(12) + .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4) + } + + // 内容预览卡片 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("内容") + .font(.caption) + .foregroundColor(.secondary) + + Spacer() + + Text(selectedQRCodeType.displayName) + .font(.caption) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.orange.opacity(0.1)) + .foregroundColor(.orange) + .cornerRadius(4) + } + + Text(formatContentForQRCodeType()) + .font(.body) + .foregroundColor(.primary) + .textSelection(.enabled) + } + .padding() + .background(Color(.systemGray6)) + .cornerRadius(8) + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 2) + } + .padding(.horizontal, 20) + } + + Spacer(minLength: 100) + } + .padding(.top, 20) + } + .background(Color(.systemGroupedBackground)) + } + + + + // MARK: - Helper Methods + + private func getCharacterCountColor() -> Color { + if content.count >= 150 { + return .orange + } else if content.count >= 140 { + return .blue + } else { + return .secondary + } + } + + private func getContentHint() -> String { + switch selectedQRCodeType { + case .text: + return "输入任意文本内容" + case .url: + return "输入网址,如:https://www.example.com" + case .mail: + return "输入邮箱地址,如:user@example.com" + case .phone: + return "输入电话号码,如:+86 138 0013 8000" + case .sms: + return "输入短信内容,如:Hello World" + case .wifi: + return "输入WiFi信息,如:SSID:MyWiFi,Password:12345678" + case .vcard: + return "输入联系人信息" + case .mecard: + return "输入联系人信息(简化版)" + case .location: + return "输入地理位置,如:40.7128,-74.0060" + case .calendar: + return "输入日历事件信息" + case .instagram: + return "输入Instagram用户名或链接" + case .facebook: + return "输入Facebook用户名或链接" + case .spotify: + return "输入Spotify歌曲或播放列表链接" + case .twitter: + return "输入Twitter用户名或链接" + case .whatsapp: + return "输入WhatsApp消息内容" + case .viber: + return "输入Viber消息内容" + case .snapchat: + return "输入Snapchat用户名" + case .tiktok: + return "输入TikTok用户名或链接" + } + } + + private func getPlaceholderText() -> String { + switch selectedQRCodeType { + case .text: + return "输入文本内容" + case .url: + return "输入网址" + case .mail: + return "输入邮箱地址" + case .phone: + return "输入电话号码" + case .sms: + return "输入短信内容" + case .wifi: + return "输入WiFi信息" + case .vcard: + return "输入联系人信息" + case .mecard: + return "输入联系人信息" + case .location: + return "输入地理位置坐标" + case .calendar: + return "输入日历事件信息" + case .instagram: + return "输入Instagram信息" + case .facebook: + return "输入Facebook信息" + case .spotify: + return "输入Spotify链接" + case .twitter: + return "输入Twitter信息" + case .whatsapp: + return "输入WhatsApp消息" + case .viber: + return "输入Viber消息" + case .snapchat: + return "输入Snapchat用户名" + case .tiktok: + return "输入TikTok信息" + } + } + + private func generateQRCodeImage() -> UIImage? { + guard !content.isEmpty else { return nil } + + // 根据二维码类型格式化内容 + let formattedContent = formatContentForQRCodeType() + + // 生成二维码 + let data = formattedContent.data(using: .utf8) + let qrFilter = CIFilter.qrCodeGenerator() + qrFilter.setValue(data, forKey: "inputMessage") + qrFilter.setValue("H", forKey: "inputCorrectionLevel") // 高纠错级别 + + guard let outputImage = qrFilter.outputImage else { return nil } + + // 转换为UIImage + let context = CIContext() + guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else { return nil } + + return UIImage(cgImage: cgImage) + } + + private func formatContentForQRCodeType() -> String { + switch selectedQRCodeType { + case .text: + return content + case .url: + return content.hasPrefix("http") ? content : "https://\(content)" + case .mail: + return "mailto:\(content)" + case .phone: + return "tel:\(content)" + case .sms: + return "sms:\(content)" + case .wifi: + return "WIFI:T:WPA;S:\(content);P:password;;" + case .vcard: + return "BEGIN:VCARD\nVERSION:3.0\nFN:\(content)\nEND:VCARD" + case .mecard: + return "MECARD:N:\(content);;" + case .location: + return "geo:\(content)" + case .calendar: + return "BEGIN:VEVENT\nSUMMARY:\(content)\nEND:VEVENT" + case .instagram: + return "https://instagram.com/\(content)" + case .facebook: + return "https://facebook.com/\(content)" + case .spotify: + return content.hasPrefix("http") ? content : "https://open.spotify.com/track/\(content)" + case .twitter: + return "https://twitter.com/\(content)" + case .whatsapp: + return "https://wa.me/\(content)" + case .viber: + return "viber://chat?number=\(content)" + case .snapchat: + return "https://snapchat.com/add/\(content)" + case .tiktok: + return "https://tiktok.com/@\(content)" + } + } + + private func createQRCode() { + guard !content.isEmpty else { return } + + let context = coreDataManager.container.viewContext + let historyItem = HistoryItem(context: context) + historyItem.id = UUID() + historyItem.content = content + historyItem.dataType = DataType.qrcode.rawValue + historyItem.dataSource = DataSource.created.rawValue + historyItem.createdAt = Date() + historyItem.isFavorite = false + historyItem.qrCodeType = selectedQRCodeType.rawValue + + do { + try context.save() + alertMessage = "二维码创建成功!" + showingAlert = true + // 创建成功后返回 + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + dismiss() + } + } catch { + alertMessage = "保存失败:\(error.localizedDescription)" + showingAlert = true + } + } +} + +#Preview { + NavigationView { + CreateQRCodeView(selectedQRCodeType: .text) + } +} \ No newline at end of file