You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
544 lines
20 KiB
544 lines
20 KiB
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 = ""
|
|
@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: EmailInputView.EmailField?
|
|
|
|
// WiFi相关字段
|
|
@State private var wifiSSID = ""
|
|
@State private var wifiPassword = ""
|
|
@State private var wifiEncryptionType: WiFiInputView.WiFiEncryptionType = .wpa2
|
|
@FocusState private var focusedWiFiField: WiFiInputView.WiFiField?
|
|
|
|
// 联系人相关字段
|
|
@State private var contactFirstName = ""
|
|
@State private var contactLastName = ""
|
|
@State private var contactPhone = ""
|
|
@State private var contactEmail = ""
|
|
@State private var contactCompany = ""
|
|
@State private var contactTitle = ""
|
|
@State private var contactAddress = ""
|
|
@State private var contactWebsite = ""
|
|
@FocusState private var focusedContactField: ContactInputView.ContactField?
|
|
|
|
// 位置相关字段
|
|
@State private var locationLatitude = ""
|
|
@State private var locationLongitude = ""
|
|
@State private var locationName = ""
|
|
@FocusState private var focusedLocationField: LocationInputView.LocationField?
|
|
|
|
// 日历相关字段
|
|
@State private var eventTitle = ""
|
|
@State private var eventDescription = ""
|
|
@State private var eventLocation = ""
|
|
@State private var startDate = Date()
|
|
@State private var endDate = Date().addingTimeInterval(3600)
|
|
@FocusState private var focusedCalendarField: CalendarInputView.CalendarField?
|
|
|
|
// 社交平台相关字段
|
|
@State private var socialUsername = ""
|
|
@State private var socialMessage = ""
|
|
@FocusState private var focusedSocialField: SocialInputView.SocialField?
|
|
|
|
// 电话相关字段
|
|
@State private var phoneNumber = ""
|
|
@State private var phoneMessage = ""
|
|
@FocusState private var focusedPhoneField: PhoneInputView.PhoneField?
|
|
|
|
// URL相关字段
|
|
@State private var urlString = ""
|
|
@FocusState private var isURLFieldFocused: Bool
|
|
|
|
// 通用状态
|
|
@State private var showingAlert = false
|
|
@State private var alertMessage = ""
|
|
|
|
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 {
|
|
setupInitialFocus()
|
|
}
|
|
.onTapGesture {
|
|
hideKeyboard()
|
|
}
|
|
}
|
|
|
|
// MARK: - UI Components
|
|
|
|
private var inputAndPreviewSection: some View {
|
|
ScrollView {
|
|
VStack(spacing: 24) {
|
|
// 输入提示
|
|
InputHintView.info(
|
|
hint: getContentHint()
|
|
)
|
|
.padding(.horizontal, 20)
|
|
|
|
// 内容输入区域
|
|
VStack(spacing: 16) {
|
|
InputTitleView.required(
|
|
selectedQRCodeType == .mail ? "邮件信息" : "输入内容",
|
|
icon: getInputIcon()
|
|
)
|
|
.padding(.horizontal, 20)
|
|
|
|
// 使用InputComponentFactory动态选择输入组件
|
|
createInputComponentForType()
|
|
.padding(.horizontal, 20)
|
|
}
|
|
|
|
// 预览区域
|
|
if canCreateQRCode() {
|
|
VStack(spacing: 16) {
|
|
InputTitleView.required("预览", icon: "eye")
|
|
.padding(.horizontal, 20)
|
|
|
|
// 使用QRCodePreviewView组件
|
|
QRCodePreviewView(
|
|
qrCodeImage: generateQRCodeImage(),
|
|
formattedContent: formatContentForQRCodeType(),
|
|
qrCodeType: selectedQRCodeType
|
|
)
|
|
.padding(.horizontal, 20)
|
|
}
|
|
}
|
|
|
|
Spacer(minLength: 100)
|
|
}
|
|
.padding(.top, 20)
|
|
}
|
|
.background(Color(.systemGroupedBackground))
|
|
}
|
|
|
|
// MARK: - Helper Methods
|
|
|
|
private func createInputComponentForType() -> AnyView {
|
|
switch selectedQRCodeType {
|
|
case .mail:
|
|
let emailConfig = EmailInputConfig(
|
|
emailAddress: $emailAddress,
|
|
emailSubject: $emailSubject,
|
|
emailBody: $emailBody,
|
|
emailCc: $emailCc,
|
|
emailBcc: $emailBcc
|
|
)
|
|
return InputComponentFactory.createInputComponent(
|
|
for: selectedQRCodeType,
|
|
emailConfig: emailConfig
|
|
)
|
|
|
|
case .wifi:
|
|
let wifiConfig = WiFiInputConfig(
|
|
ssid: $wifiSSID,
|
|
password: $wifiPassword,
|
|
encryptionType: $wifiEncryptionType
|
|
)
|
|
return InputComponentFactory.createInputComponent(
|
|
for: selectedQRCodeType,
|
|
wifiConfig: wifiConfig
|
|
)
|
|
|
|
case .vcard, .mecard:
|
|
let contactConfig = ContactInputConfig(
|
|
firstName: $contactFirstName,
|
|
lastName: $contactLastName,
|
|
phone: $contactPhone,
|
|
email: $contactEmail,
|
|
company: $contactCompany,
|
|
title: $contactTitle,
|
|
address: $contactAddress,
|
|
website: $contactWebsite
|
|
)
|
|
return InputComponentFactory.createInputComponent(
|
|
for: selectedQRCodeType,
|
|
contactConfig: contactConfig
|
|
)
|
|
|
|
case .location:
|
|
let locationConfig = LocationInputConfig(
|
|
latitude: $locationLatitude,
|
|
longitude: $locationLongitude,
|
|
locationName: $locationName
|
|
)
|
|
return InputComponentFactory.createInputComponent(
|
|
for: selectedQRCodeType,
|
|
locationConfig: locationConfig
|
|
)
|
|
|
|
case .calendar:
|
|
let calendarConfig = CalendarInputConfig(
|
|
eventTitle: $eventTitle,
|
|
eventDescription: $eventDescription,
|
|
startDate: $startDate,
|
|
endDate: $endDate,
|
|
location: $eventLocation
|
|
)
|
|
return InputComponentFactory.createInputComponent(
|
|
for: selectedQRCodeType,
|
|
calendarConfig: calendarConfig
|
|
)
|
|
|
|
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok, .whatsapp, .viber:
|
|
let socialConfig = SocialInputConfig(
|
|
username: $socialUsername,
|
|
message: $socialMessage
|
|
)
|
|
return InputComponentFactory.createInputComponent(
|
|
for: selectedQRCodeType,
|
|
socialConfig: socialConfig
|
|
)
|
|
|
|
case .phone, .sms:
|
|
let phoneConfig = PhoneInputConfig(
|
|
phoneNumber: $phoneNumber,
|
|
phoneMessage: $phoneMessage
|
|
)
|
|
return InputComponentFactory.createInputComponent(
|
|
for: selectedQRCodeType,
|
|
phoneConfig: phoneConfig
|
|
)
|
|
|
|
case .url:
|
|
let urlConfig = URLInputConfig(
|
|
url: $urlString
|
|
)
|
|
return InputComponentFactory.createInputComponent(
|
|
for: selectedQRCodeType,
|
|
urlConfig: urlConfig
|
|
)
|
|
|
|
default:
|
|
let textConfig = TextInputConfig(
|
|
content: $content
|
|
)
|
|
return InputComponentFactory.createInputComponent(
|
|
for: selectedQRCodeType,
|
|
textConfig: textConfig
|
|
)
|
|
}
|
|
}
|
|
|
|
private func setupInitialFocus() {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
switch selectedQRCodeType {
|
|
case .mail:
|
|
focusedEmailField = .address
|
|
case .wifi:
|
|
focusedWiFiField = .ssid
|
|
case .vcard, .mecard:
|
|
focusedContactField = .firstName
|
|
case .location:
|
|
focusedLocationField = .latitude
|
|
case .calendar:
|
|
focusedCalendarField = .title
|
|
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok:
|
|
focusedSocialField = .username
|
|
case .phone, .sms:
|
|
focusedPhoneField = .phoneNumber
|
|
case .url:
|
|
isURLFieldFocused = true
|
|
default:
|
|
isContentFieldFocused = true
|
|
}
|
|
}
|
|
}
|
|
|
|
private func hideKeyboard() {
|
|
switch selectedQRCodeType {
|
|
case .mail:
|
|
focusedEmailField = nil
|
|
case .wifi:
|
|
focusedWiFiField = nil
|
|
case .vcard, .mecard:
|
|
focusedContactField = nil
|
|
case .location:
|
|
focusedLocationField = nil
|
|
case .calendar:
|
|
focusedCalendarField = nil
|
|
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok:
|
|
focusedSocialField = nil
|
|
case .phone, .sms:
|
|
focusedPhoneField = nil
|
|
case .url:
|
|
isURLFieldFocused = false
|
|
default:
|
|
isContentFieldFocused = false
|
|
}
|
|
}
|
|
|
|
private func getInputIcon() -> String {
|
|
switch selectedQRCodeType {
|
|
case .mail: return "envelope"
|
|
case .wifi: return "wifi"
|
|
case .vcard, .mecard: return "person"
|
|
case .location: return "location"
|
|
case .calendar: return "calendar"
|
|
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok, .whatsapp, .viber: return "globe"
|
|
case .phone, .sms: return "phone"
|
|
case .url: return "link"
|
|
default: return "textformat"
|
|
}
|
|
}
|
|
|
|
private func canCreateQRCode() -> Bool {
|
|
switch selectedQRCodeType {
|
|
case .mail:
|
|
return !emailAddress.isEmpty && !emailSubject.isEmpty && !emailBody.isEmpty
|
|
case .wifi:
|
|
return !wifiSSID.isEmpty
|
|
case .vcard, .mecard:
|
|
return !contactFirstName.isEmpty || !contactLastName.isEmpty
|
|
case .location:
|
|
return !locationLatitude.isEmpty && !locationLongitude.isEmpty
|
|
case .calendar:
|
|
return !eventTitle.isEmpty
|
|
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok, .whatsapp, .viber:
|
|
return !socialUsername.isEmpty
|
|
case .phone, .sms:
|
|
return !phoneNumber.isEmpty
|
|
case .url:
|
|
return !urlString.isEmpty
|
|
default:
|
|
return !content.isEmpty
|
|
}
|
|
}
|
|
|
|
private func getContentHint() -> String {
|
|
InputComponentFactory.getPlaceholderText(for: selectedQRCodeType)
|
|
}
|
|
|
|
private func generateQRCodeImage() -> UIImage? {
|
|
guard canCreateQRCode() 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 }
|
|
|
|
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 urlString.hasPrefix("http") ? urlString : "https://\(urlString)"
|
|
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:\(phoneNumber)"
|
|
case .sms:
|
|
let smsContent = phoneMessage.isEmpty ? "Hello" : phoneMessage
|
|
return "sms:\(phoneNumber):\(smsContent)"
|
|
case .wifi:
|
|
return "WIFI:T:\(wifiEncryptionType.rawValue);S:\(wifiSSID);P:\(wifiPassword);;"
|
|
case .vcard:
|
|
var vcard = "BEGIN:VCARD\nVERSION:3.0\n"
|
|
if !contactFirstName.isEmpty || !contactLastName.isEmpty {
|
|
vcard += "FN:\(contactFirstName) \(contactLastName)\n"
|
|
}
|
|
if !contactPhone.isEmpty {
|
|
vcard += "TEL:\(contactPhone)\n"
|
|
}
|
|
if !contactEmail.isEmpty {
|
|
vcard += "EMAIL:\(contactEmail)\n"
|
|
}
|
|
if !contactCompany.isEmpty {
|
|
vcard += "ORG:\(contactCompany)\n"
|
|
}
|
|
if !contactTitle.isEmpty {
|
|
vcard += "TITLE:\(contactTitle)\n"
|
|
}
|
|
if !contactAddress.isEmpty {
|
|
vcard += "ADR:\(contactAddress)\n"
|
|
}
|
|
if !contactWebsite.isEmpty {
|
|
vcard += "URL:\(contactWebsite)\n"
|
|
}
|
|
vcard += "END:VCARD"
|
|
return vcard
|
|
case .mecard:
|
|
var mecard = "MECARD:"
|
|
if !contactFirstName.isEmpty || !contactLastName.isEmpty {
|
|
mecard += "N:\(contactLastName),\(contactFirstName);"
|
|
}
|
|
if !contactPhone.isEmpty {
|
|
mecard += "TEL:\(contactPhone);"
|
|
}
|
|
if !contactEmail.isEmpty {
|
|
mecard += "EMAIL:\(contactEmail);"
|
|
}
|
|
if !contactCompany.isEmpty {
|
|
mecard += "ORG:\(contactCompany);"
|
|
}
|
|
if !contactAddress.isEmpty {
|
|
mecard += "ADR:\(contactAddress);"
|
|
}
|
|
if !contactWebsite.isEmpty {
|
|
mecard += "URL:\(contactWebsite);"
|
|
}
|
|
mecard += ";"
|
|
return mecard
|
|
case .location:
|
|
let coords = "\(locationLatitude),\(locationLongitude)"
|
|
return locationName.isEmpty ? "geo:\(coords)" : "geo:\(coords)?q=\(locationName)"
|
|
case .calendar:
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
|
|
dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
|
|
|
|
var ical = "BEGIN:VEVENT\n"
|
|
ical += "SUMMARY:\(eventTitle)\n"
|
|
if !eventDescription.isEmpty {
|
|
ical += "DESCRIPTION:\(eventDescription)\n"
|
|
}
|
|
if !eventLocation.isEmpty {
|
|
ical += "LOCATION:\(eventLocation)\n"
|
|
}
|
|
ical += "DTSTART:\(dateFormatter.string(from: startDate))\n"
|
|
ical += "DTEND:\(dateFormatter.string(from: endDate))\n"
|
|
ical += "END:VEVENT"
|
|
return ical
|
|
case .instagram:
|
|
return "https://instagram.com/\(socialUsername)"
|
|
case .facebook:
|
|
return "https://facebook.com/\(socialUsername)"
|
|
case .spotify:
|
|
return socialUsername.hasPrefix("http") ? socialUsername : "https://open.spotify.com/track/\(socialUsername)"
|
|
case .twitter:
|
|
return "https://twitter.com/\(socialUsername)"
|
|
case .whatsapp:
|
|
let message = socialMessage.isEmpty ? "Hello" : socialMessage
|
|
return "https://wa.me/\(socialUsername)?text=\(message.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? message)"
|
|
case .viber:
|
|
let message = socialMessage.isEmpty ? "Hello" : socialMessage
|
|
return "viber://chat?number=\(socialUsername)&text=\(message.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? message)"
|
|
case .snapchat:
|
|
return "https://snapchat.com/add/\(socialUsername)"
|
|
case .tiktok:
|
|
return "https://tiktok.com/@\(socialUsername)"
|
|
}
|
|
}
|
|
|
|
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
|
|
case .wifi:
|
|
historyItem.content = "WiFi: \(wifiSSID) (\(wifiEncryptionType.displayName))"
|
|
case .vcard, .mecard:
|
|
historyItem.content = "联系人: \(contactFirstName) \(contactLastName)"
|
|
case .location:
|
|
historyItem.content = "位置: \(locationLatitude), \(locationLongitude)"
|
|
case .calendar:
|
|
historyItem.content = "事件: \(eventTitle)"
|
|
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok, .whatsapp, .viber:
|
|
historyItem.content = "\(selectedQRCodeType.displayName): \(socialUsername)"
|
|
case .phone, .sms:
|
|
historyItem.content = "电话: \(phoneNumber)"
|
|
case .url:
|
|
historyItem.content = "URL: \(urlString)"
|
|
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)
|
|
}
|
|
}
|