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 // Email相关字段 @State private var emailAddress = "" @State private var emailSubject = "" @State private var emailBody = "" @State private var emailCc = "" @State private var emailBcc = "" @FocusState private var focusedEmailField: EmailField? // Email字段枚举 private enum EmailField: Hashable { case address, subject, body, cc, bcc } var body: some View { VStack(spacing: 0) { inputAndPreviewSection } .navigationTitle(selectedQRCodeType.displayName) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("创建") { createQRCode() } .disabled(!canCreateQRCode()) .font(.system(size: 16, weight: .semibold)) } } .alert("提示", isPresented: $showingAlert) { Button("确定") { } } message: { Text(alertMessage) } .onAppear { // 稍延迟以确保进入页面时自动聚焦 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { if selectedQRCodeType == .mail { focusedEmailField = .address } else { isContentFieldFocused = true } } } .onTapGesture { // 点击外部关闭键盘 if selectedQRCodeType == .mail { focusedEmailField = nil } else { isContentFieldFocused = false } } } // MARK: - UI Components private var emailInputSection: some View { VStack(spacing: 16) { // Email地址 (必填) VStack(alignment: .leading, spacing: 8) { HStack { Text("邮箱地址") .font(.subheadline) .foregroundColor(.primary) Text("*") .foregroundColor(.red) Spacer() } TextField("user@example.com", text: $emailAddress) .textFieldStyle(RoundedBorderTextFieldStyle()) .keyboardType(.emailAddress) .autocapitalization(.none) .focused($focusedEmailField, equals: .address) } // 主题 (必填) VStack(alignment: .leading, spacing: 8) { HStack { Text("主题") .font(.subheadline) .foregroundColor(.primary) Text("*") .foregroundColor(.red) Spacer() } TextField("邮件主题", text: $emailSubject) .textFieldStyle(RoundedBorderTextFieldStyle()) .focused($focusedEmailField, equals: .subject) } // 正文 (必填) VStack(alignment: .leading, spacing: 8) { HStack { Text("正文") .font(.subheadline) .foregroundColor(.primary) Text("*") .foregroundColor(.red) Spacer() } ZStack { TextEditor(text: $emailBody) .frame(minHeight: 120) .padding(8) .background(Color(.systemBackground)) .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(focusedEmailField == .body ? Color.blue : Color(.systemGray4), lineWidth: 1) ) .focused($focusedEmailField, equals: .body) .onChange(of: emailBody) { newValue in // 限制最大字符数为1200 if newValue.count > 1200 { emailBody = String(newValue.prefix(1200)) } } // 占位符文本 if emailBody.isEmpty && focusedEmailField != .body { VStack { HStack { Text("输入邮件正文内容...") .foregroundColor(.secondary) .font(.body) Spacer() } Spacer() } .padding(16) .allowsHitTesting(false) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } } // 字符计数 HStack { Spacer() Text("\(emailBody.count)/1200") .font(.caption) .foregroundColor(emailBody.count >= 1200 ? .orange : .secondary) } } // CC地址 (可选) VStack(alignment: .leading, spacing: 8) { HStack { Text("抄送地址") .font(.subheadline) .foregroundColor(.primary) Spacer() } TextField("cc@example.com", text: $emailCc) .textFieldStyle(RoundedBorderTextFieldStyle()) .keyboardType(.emailAddress) .autocapitalization(.none) .focused($focusedEmailField, equals: .cc) } // BCC地址 (可选) VStack(alignment: .leading, spacing: 8) { HStack { Text("密送地址") .font(.subheadline) .foregroundColor(.primary) Spacer() } TextField("bcc@example.com", text: $emailBcc) .textFieldStyle(RoundedBorderTextFieldStyle()) .keyboardType(.emailAddress) .autocapitalization(.none) .focused($focusedEmailField, equals: .bcc) } } .toolbar { ToolbarItemGroup(placement: .keyboard) { Spacer() Button("完成") { focusedEmailField = nil } .foregroundColor(.blue) .font(.system(size: 16, weight: .medium)) } } } 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(selectedQRCodeType == .mail ? "邮件信息" : "输入内容") .font(.headline) .foregroundColor(.primary) Spacer() } if selectedQRCodeType == .mail { // Email专用输入界面 emailInputSection } else { // 通用输入界面 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() // Pushes content to the right 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 selectedQRCodeType == .mail ? (!emailAddress.isEmpty && !emailSubject.isEmpty && !emailBody.isEmpty) : !content.isEmpty { VStack(spacing: 16) { HStack { Text("预览") .font(.headline) .foregroundColor(.primary) Spacer() } // 二维码预览卡片 VStack(spacing: 16) { // 二维码图片 if let qrImage = generateQRCodeImage() { Image(uiImage: qrImage) .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 canCreateQRCode() -> Bool { switch selectedQRCodeType { case .mail: // Email类型:邮箱地址、主题、正文为必填 return !emailAddress.isEmpty && !emailSubject.isEmpty && !emailBody.isEmpty default: // 其他类型:内容不能为空 return !content.isEmpty } } 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 "填写邮件信息,邮箱地址、主题、正文为必填项" 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? { // 根据二维码类型检查内容是否为空 let hasContent: Bool switch selectedQRCodeType { case .mail: hasContent = !emailAddress.isEmpty && !emailSubject.isEmpty && !emailBody.isEmpty default: hasContent = !content.isEmpty } guard hasContent 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: var mailtoURL = "mailto:\(emailAddress)" var queryParams: [String] = [] if !emailSubject.isEmpty { queryParams.append("subject=\(emailSubject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? emailSubject)") } if !emailBody.isEmpty { queryParams.append("body=\(emailBody.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? emailBody)") } if !emailCc.isEmpty { queryParams.append("cc=\(emailCc)") } if !emailBcc.isEmpty { queryParams.append("bcc=\(emailBcc)") } if !queryParams.isEmpty { mailtoURL += "?" + queryParams.joined(separator: "&") } return mailtoURL 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() { let context = coreDataManager.container.viewContext let historyItem = HistoryItem(context: context) historyItem.id = UUID() historyItem.dataType = DataType.qrcode.rawValue historyItem.dataSource = DataSource.created.rawValue historyItem.createdAt = Date() historyItem.isFavorite = false historyItem.qrCodeType = selectedQRCodeType.rawValue // 根据类型设置内容 switch selectedQRCodeType { case .mail: var mailContent = "邮箱: \(emailAddress)\n主题: \(emailSubject)\n正文: \(emailBody)" if !emailCc.isEmpty { mailContent += "\n抄送: \(emailCc)" } if !emailBcc.isEmpty { mailContent += "\n密送: \(emailBcc)" } historyItem.content = mailContent default: historyItem.content = content } 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: .mail) } }