import Foundation import UIKit // MARK: - vCard版本转换工具 struct VCardConverter { /// 将vCard 2.1格式转换为3.0格式 static func convertVCard21To30(_ vcard21: String) -> String { let lines = vcard21.components(separatedBy: .newlines) var vcard30 = "BEGIN:VCARD\nVERSION:3.0\n" for line in lines { let trimmedLine = line.trimmingCharacters(in: .whitespaces) if trimmedLine.isEmpty || trimmedLine.hasPrefix("BEGIN:") || trimmedLine.hasPrefix("VERSION:") || trimmedLine.hasPrefix("END:") { continue } // 处理N字段 (姓名) if trimmedLine.hasPrefix("N:") { let nameValue = String(trimmedLine.dropFirst(2)) let nameParts = nameValue.components(separatedBy: ";") if nameParts.count >= 2 { let lastName = nameParts[0] let firstName = nameParts[1] vcard30 += "N:\(lastName);\(firstName);;;\n" vcard30 += "FN:\(firstName) \(lastName)\n" } } // 处理TEL字段 (电话) else if trimmedLine.hasPrefix("TEL") { let telValue = String(trimmedLine.dropFirst(3)) if telValue.hasPrefix(";TYPE=") { let typeStart = telValue.firstIndex(of: "=")! let typeEnd = telValue.firstIndex(of: ":") ?? telValue.endIndex let type = String(telValue[telValue.index(after: typeStart).. String { let lines = vcard.components(separatedBy: .newlines) for line in lines { if line.hasPrefix("VERSION:") { return String(line.dropFirst(8)) } } return "3.0" // 默认版本 } /// 标准化vCard为3.0版本 static func normalizeVCard(_ vcard: String) -> String { let version = detectVCardVersion(vcard) if version == "2.1" { return convertVCard21To30(vcard) } return vcard } /// 测试vCard版本转换功能 static func testVCardConversion() { let vcard21 = """ BEGIN:VCARD VERSION:2.1 N:Surname;Givenname;;; FN:Givenname Surname TEL;TYPE=WORK:123-456-7890 EMAIL;TYPE=PREF:test@example.com END:VCARD """ let converted = convertVCard21To30(vcard21) print("Original vCard 2.1:") print(vcard21) print("\nConverted to vCard 3.0:") print(converted) } } // MARK: - 二维码解析器 class QRCodeParser { // MARK: - 解析二维码内容 static func parseQRCode(_ content: String) -> ParsedQRData { let trimmedContent = content.trimmingCharacters(in: .whitespacesAndNewlines) // Wi-Fi if trimmedContent.hasPrefix("WIFI:") { return parseWiFi(trimmedContent) } // Email if trimmedContent.hasPrefix("mailto:") { return parseEmail(trimmedContent) } // Phone if trimmedContent.hasPrefix("tel:") { return parsePhone(trimmedContent) } // SMS if trimmedContent.hasPrefix("SMSTO:") { return parseSMS(trimmedContent) } // vCard if trimmedContent.hasPrefix("BEGIN:VCARD") { return parseVCard(trimmedContent) } // MeCard if trimmedContent.hasPrefix("MECARD:") { return parseMeCard(trimmedContent) } // Calendar if trimmedContent.hasPrefix("BEGIN:VEVENT") { return parseCalendar(trimmedContent) } // Instagram if trimmedContent.hasPrefix("instagram://user?username=") { return parseInstagram(trimmedContent) } // Facebook if trimmedContent.hasPrefix("fb://profile/") { return parseFacebook(trimmedContent) } // Spotify if trimmedContent.hasPrefix("spotify:search:") { return parseSpotify(trimmedContent) } // X (Twitter) if trimmedContent.hasPrefix("twitter://user?screen_name=") || trimmedContent.contains("x.com") || trimmedContent.contains("twitter.com") { return parseTwitter(trimmedContent) } // WhatsApp if trimmedContent.hasPrefix("whatsapp://send?phone=") { return parseWhatsApp(trimmedContent) } // Viber if trimmedContent.hasPrefix("viber://add?number=") { return parseViber(trimmedContent) } // Snapchat if trimmedContent.hasPrefix("snapchat://") { return parseSnapchat(trimmedContent) } // TikTok if trimmedContent.contains("tiktok.com") || trimmedContent.contains("www.tiktok.com") { return parseTikTok(trimmedContent) } // URL (检查是否为有效的URL) if isValidURL(trimmedContent) { return parseURL(trimmedContent) } // Location if trimmedContent.hasPrefix("geo:") { return parseLocation(trimmedContent) } // 默认为文本类型 return ParsedQRData( type: .text, title: "文本信息", subtitle: trimmedContent.count > 50 ? String(trimmedContent.prefix(50)) + "..." : trimmedContent, icon: "text.quote" ) } // MARK: - 解析Wi-Fi private static func parseWiFi(_ content: String) -> ParsedQRData { let wifiInfo = content.replacingOccurrences(of: "WIFI:", with: "") let components = wifiInfo.components(separatedBy: ";") var ssid = "" var password = "" var encryption = "WPA" for component in components { if component.hasPrefix("S:") { ssid = String(component.dropFirst(2)) } else if component.hasPrefix("P:") { password = String(component.dropFirst(2)) } else if component.hasPrefix("T:") { encryption = String(component.dropFirst(2)) } } let title = "Wi-Fi网络" let subtitle = "网络名称: \(ssid)\n加密类型: \(encryption)\n密码: \(password.isEmpty ? "无" : "已设置")" return ParsedQRData( type: .wifi, title: title, subtitle: subtitle, icon: "wifi" ) } // MARK: - 解析Email private static func parseEmail(_ content: String) -> ParsedQRData { let email = content.replacingOccurrences(of: "mailto:", with: "") return ParsedQRData( type: .mail, title: "邮箱地址", subtitle: email, icon: "envelope" ) } // MARK: - 解析Phone private static func parsePhone(_ content: String) -> ParsedQRData { let phone = content.replacingOccurrences(of: "tel:", with: "") return ParsedQRData( type: .phone, title: "电话号码", subtitle: phone, icon: "phone" ) } // MARK: - 解析SMS private static func parseSMS(_ content: String) -> ParsedQRData { let smsInfo = content.replacingOccurrences(of: "SMSTO:", with: "") let components = smsInfo.components(separatedBy: ":") let phone = components.first ?? "" let message = components.count > 1 ? components[1] : "" let title = "短信" let subtitle = "号码: \(phone)\n内容: \(message)" return ParsedQRData( type: .sms, title: title, subtitle: subtitle, icon: "message" ) } // MARK: - 解析vCard private static func parseVCard(_ content: String) -> ParsedQRData { // 标准化vCard为3.0版本 let normalizedVCard = VCardConverter.normalizeVCard(content) let lines = normalizedVCard.components(separatedBy: .newlines) var name = "" var phone = "" var email = "" var company = "" var title = "" var address = "" var website = "" for line in lines { let trimmedLine = line.trimmingCharacters(in: .whitespaces) if trimmedLine.hasPrefix("FN:") { name = String(trimmedLine.dropFirst(3)) } else if trimmedLine.hasPrefix("TEL") { let telValue = String(trimmedLine.dropFirst(3)) if telValue.contains(":") { let number = telValue.components(separatedBy: ":").last ?? "" phone = number } } else if trimmedLine.hasPrefix("EMAIL") { let emailValue = String(trimmedLine.dropFirst(5)) if emailValue.contains(":") { let emailAddress = emailValue.components(separatedBy: ":").last ?? "" email = emailAddress } } else if trimmedLine.hasPrefix("ORG:") { company = String(trimmedLine.dropFirst(4)) } else if trimmedLine.hasPrefix("TITLE:") { title = String(trimmedLine.dropFirst(6)) } else if trimmedLine.hasPrefix("ADR") { let adrValue = String(trimmedLine.dropFirst(3)) if adrValue.contains(":") { let addressParts = adrValue.components(separatedBy: ":") if addressParts.count > 1 { let addressComponents = addressParts[1].components(separatedBy: ";") if addressComponents.count >= 3 { address = "\(addressComponents[2]) \(addressComponents[1])" } } } } else if trimmedLine.hasPrefix("URL:") { website = String(trimmedLine.dropFirst(4)) } } var subtitle = "" if !name.isEmpty { subtitle += "姓名: \(name)\n" } if !phone.isEmpty { subtitle += "电话: \(phone)\n" } if !email.isEmpty { subtitle += "邮箱: \(email)\n" } if !company.isEmpty { subtitle += "公司: \(company)\n" } if !title.isEmpty { subtitle += "职位: \(title)\n" } if !address.isEmpty { subtitle += "地址: \(address)\n" } if !website.isEmpty { subtitle += "网站: \(website)\n" } // 移除最后一个换行符 if subtitle.hasSuffix("\n") { subtitle = String(subtitle.dropLast()) } return ParsedQRData( type: .vcard, title: "联系人信息", subtitle: subtitle, icon: "person.crop.rectangle" ) } // MARK: - 解析MeCard private static func parseMeCard(_ content: String) -> ParsedQRData { let mecardInfo = content.replacingOccurrences(of: "MECARD:", with: "") let components = mecardInfo.components(separatedBy: ";") var name = "" var nickname = "" var phone = "" var email = "" var company = "" var address = "" var website = "" var birthday = "" var note = "" for component in components { let trimmedComponent = component.trimmingCharacters(in: .whitespaces) if trimmedComponent.isEmpty { continue } if trimmedComponent.hasPrefix("N:") { let nameValue = String(trimmedComponent.dropFirst(2)) let nameParts = nameValue.components(separatedBy: ",") if nameParts.count >= 2 { let lastName = nameParts[0] let firstName = nameParts[1] name = "\(firstName) \(lastName)" } else if nameParts.count == 1 { name = nameParts[0] } } else if trimmedComponent.hasPrefix("NICKNAME:") { nickname = String(trimmedComponent.dropFirst(9)) } else if trimmedComponent.hasPrefix("TEL:") { phone = String(trimmedComponent.dropFirst(4)) } else if trimmedComponent.hasPrefix("EMAIL:") { email = String(trimmedComponent.dropFirst(6)) } else if trimmedComponent.hasPrefix("ORG:") { company = String(trimmedComponent.dropFirst(4)) } else if trimmedComponent.hasPrefix("ADR:") { address = String(trimmedComponent.dropFirst(4)) } else if trimmedComponent.hasPrefix("URL:") { website = String(trimmedComponent.dropFirst(4)) } else if trimmedComponent.hasPrefix("BDAY:") { let birthdayValue = String(trimmedComponent.dropFirst(5)) if birthdayValue.count == 8 { let year = String(birthdayValue.prefix(4)) let month = String(birthdayValue.dropFirst(4).prefix(2)) let day = String(birthdayValue.dropFirst(6)) birthday = "\(year)年\(month)月\(day)日" } else { birthday = birthdayValue } } else if trimmedComponent.hasPrefix("NOTE:") { note = String(trimmedComponent.dropFirst(5)) } } var subtitle = "" if !name.isEmpty { subtitle += "姓名: \(name)\n" } if !nickname.isEmpty { subtitle += "昵称: \(nickname)\n" } if !phone.isEmpty { subtitle += "电话: \(phone)\n" } if !email.isEmpty { subtitle += "邮箱: \(email)\n" } if !company.isEmpty { subtitle += "公司: \(company)\n" } if !address.isEmpty { subtitle += "地址: \(address)\n" } if !website.isEmpty { subtitle += "网站: \(website)\n" } if !birthday.isEmpty { subtitle += "生日: \(birthday)\n" } if !note.isEmpty { subtitle += "备注: \(note)\n" } // 移除最后一个换行符 if subtitle.hasSuffix("\n") { subtitle = String(subtitle.dropLast()) } return ParsedQRData( type: .mecard, title: "联系人信息", subtitle: subtitle, icon: "person.crop.rectangle" ) } // MARK: - 解析Calendar private static func parseCalendar(_ content: String) -> ParsedQRData { let lines = content.components(separatedBy: .newlines) var summary = "" var startTime = "" var endTime = "" var location = "" var description = "" for line in lines { if line.hasPrefix("SUMMARY:") { summary = String(line.dropFirst(8)) } else if line.hasPrefix("DTSTART:") { startTime = String(line.dropFirst(8)) } else if line.hasPrefix("DTEND:") { endTime = String(line.dropFirst(6)) } else if line.hasPrefix("LOCATION:") { location = String(line.dropFirst(9)) } else if line.hasPrefix("DESCRIPTION:") { description = String(line.dropFirst(12)) } } // 格式化时间显示 let formattedStartTime = formatCalendarTime(startTime) let formattedEndTime = formatCalendarTime(endTime) let title = "日历事件" var subtitle = "事件: \(summary)\n开始: \(formattedStartTime)\n结束: \(formattedEndTime)" if !location.isEmpty { subtitle += "\n地点: \(location)" } if !description.isEmpty { subtitle += "\n描述: \(description)" } return ParsedQRData( type: .calendar, title: title, subtitle: subtitle, icon: "calendar" ) } // MARK: - 格式化日历时间 private static func formatCalendarTime(_ timeString: String) -> String { guard timeString.count >= 15 else { return timeString } let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss" if let date = dateFormatter.date(from: timeString) { let displayFormatter = DateFormatter() displayFormatter.dateStyle = .medium displayFormatter.timeStyle = .short return displayFormatter.string(from: date) } return timeString } // MARK: - 解析Instagram private static func parseInstagram(_ content: String) -> ParsedQRData { let username = content.replacingOccurrences(of: "instagram://user?username=", with: "") return ParsedQRData( type: .instagram, title: "Instagram", subtitle: "用户名: \(username)", icon: "camera" ) } // MARK: - 解析Facebook private static func parseFacebook(_ content: String) -> ParsedQRData { let profileId = content.replacingOccurrences(of: "fb://profile/", with: "") return ParsedQRData( type: .facebook, title: "Facebook", subtitle: "用户ID: \(profileId)", icon: "person.2" ) } // MARK: - 解析Spotify private static func parseSpotify(_ content: String) -> ParsedQRData { let searchQuery = content.replacingOccurrences(of: "spotify:search:", with: "") return ParsedQRData( type: .spotify, title: "Spotify", subtitle: "搜索: \(searchQuery)", icon: "music.note" ) } // MARK: - 解析X (Twitter) private static func parseTwitter(_ content: String) -> ParsedQRData { var username = "" if content.hasPrefix("twitter://user?screen_name=") { username = content.replacingOccurrences(of: "twitter://user?screen_name=", with: "") } else if content.contains("x.com") || content.contains("twitter.com") { username = content.components(separatedBy: "/").dropLast().last ?? "" } return ParsedQRData( type: .twitter, title: "X", subtitle: "用户名: \(username)", icon: "bird" ) } // MARK: - 解析WhatsApp private static func parseWhatsApp(_ content: String) -> ParsedQRData { let phone = content.replacingOccurrences(of: "whatsapp://send?phone=", with: "") return ParsedQRData( type: .whatsapp, title: "WhatsApp", subtitle: "电话号码: \(phone)", icon: "message.circle" ) } // MARK: - 解析Viber private static func parseViber(_ content: String) -> ParsedQRData { let phone = content.replacingOccurrences(of: "viber://add?number=", with: "") return ParsedQRData( type: .viber, title: "Viber", subtitle: "电话号码: \(phone)", icon: "bubble.left.and.bubble.right" ) } // MARK: - 解析Snapchat private static func parseSnapchat(_ content: String) -> ParsedQRData { let username = content.replacingOccurrences(of: "snapchat://", with: "") return ParsedQRData( type: .snapchat, title: "Snapchat", subtitle: "用户名: \(username)", icon: "camera.viewfinder" ) } // MARK: - 解析TikTok private static func parseTikTok(_ content: String) -> ParsedQRData { var username = "" if content.contains("www.tiktok.com") { // 处理 https://www.tiktok.com/@username 格式 if let atIndex = content.lastIndex(of: "@") { username = String(content[content.index(after: atIndex)...]) } } else if content.contains("tiktok.com") { // 处理 https://tiktok.com/@username 格式 if let atIndex = content.lastIndex(of: "@") { username = String(content[content.index(after: atIndex)...]) } } return ParsedQRData( type: .tiktok, title: "TikTok", subtitle: "用户名: \(username)", icon: "music.mic" ) } // MARK: - 解析URL private static func parseURL(_ content: String) -> ParsedQRData { return ParsedQRData( type: .url, title: "网址链接", subtitle: content, icon: "link" ) } // MARK: - 解析Location private static func parseLocation(_ content: String) -> ParsedQRData { let coordinates = content.replacingOccurrences(of: "geo:", with: "") let coords = coordinates.components(separatedBy: ",") let latitude = coords.first ?? "" let longitude = coords.count > 1 ? coords[1] : "" let title = "地理位置" let subtitle = "纬度: \(latitude)\n经度: \(longitude)" return ParsedQRData( type: .location, title: title, subtitle: subtitle, icon: "location" ) } // MARK: - 验证URL private static func isValidURL(_ string: String) -> Bool { guard let url = URL(string: string) else { return false } return UIApplication.shared.canOpenURL(url) } }