From 4464897237241173b4b653b26e49fe88d32cef50 Mon Sep 17 00:00:00 2001 From: v504 Date: Fri, 22 Aug 2025 18:01:48 +0800 Subject: [PATCH] Implement vCard 2.1 to 3.0 conversion in QRCodeParser and enhance CreateQRCodeView for improved vCard generation; update QRCodeDetailView and documentation to reflect new vCard structure and support additional fields like company, title, and address. --- MyQrCode/Models/QRCodeParser.swift | 188 ++++++++++++++++++++++++-- MyQrCode/Views/CreateQRCodeView.swift | 26 +++- MyQrCode/Views/QRCodeDetailView.swift | 9 +- docs/QRCODE_DETAIL_VIEW_README.md | 11 +- 4 files changed, 216 insertions(+), 18 deletions(-) diff --git a/MyQrCode/Models/QRCodeParser.swift b/MyQrCode/Models/QRCodeParser.swift index 51a7a1c..68b2949 100644 --- a/MyQrCode/Models/QRCodeParser.swift +++ b/MyQrCode/Models/QRCodeParser.swift @@ -1,6 +1,130 @@ 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 { @@ -177,27 +301,71 @@ class QRCodeParser { // MARK: - 解析vCard private static func parseVCard(_ content: String) -> ParsedQRData { - let lines = content.components(separatedBy: .newlines) + // 标准化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 { - if line.hasPrefix("FN:") { - name = String(line.dropFirst(3)) - } else if line.hasPrefix("TEL:") { - phone = String(line.dropFirst(4)) - } else if line.hasPrefix("EMAIL:") { - email = String(line.dropFirst(6)) + 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)) } } - let title = "联系人信息" - let subtitle = "姓名: \(name)\n电话: \(phone)\n邮箱: \(email)" + 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: title, + title: "联系人信息", subtitle: subtitle, icon: "person.crop.rectangle" ) diff --git a/MyQrCode/Views/CreateQRCodeView.swift b/MyQrCode/Views/CreateQRCodeView.swift index 81c6e2f..38699e3 100644 --- a/MyQrCode/Views/CreateQRCodeView.swift +++ b/MyQrCode/Views/CreateQRCodeView.swift @@ -390,27 +390,45 @@ struct CreateQRCodeView: View { return "WIFI:T:\(wifiEncryptionType.rawValue);S:\(wifiSSID);P:\(wifiPassword);;" case .vcard: var vcard = "BEGIN:VCARD\nVERSION:3.0\n" + + // 姓名字段 (N和FN) if !contactFirstName.isEmpty || !contactLastName.isEmpty { - vcard += "FN:\(contactFirstName) \(contactLastName)\n" + let lastName = contactLastName.isEmpty ? "" : contactLastName + let firstName = contactFirstName.isEmpty ? "" : contactFirstName + vcard += "N:\(lastName);\(firstName);;;\n" + vcard += "FN:\(firstName) \(lastName)\n" } + + // 电话字段 if !contactPhone.isEmpty { - vcard += "TEL:\(contactPhone)\n" + vcard += "TEL;TYPE=WORK,CELL:\(contactPhone)\n" } + + // 邮箱字段 if !contactEmail.isEmpty { - vcard += "EMAIL:\(contactEmail)\n" + vcard += "EMAIL;TYPE=PREF,INTERNET:\(contactEmail)\n" } + + // 公司字段 if !contactCompany.isEmpty { vcard += "ORG:\(contactCompany)\n" } + + // 职位字段 if !contactTitle.isEmpty { vcard += "TITLE:\(contactTitle)\n" } + + // 地址字段 if !contactAddress.isEmpty { - vcard += "ADR:\(contactAddress)\n" + vcard += "ADR;TYPE=WORK:;;\(contactAddress);;;;\n" } + + // 网站字段 if !contactWebsite.isEmpty { vcard += "URL:\(contactWebsite)\n" } + vcard += "END:VCARD" return vcard case .mecard: diff --git a/MyQrCode/Views/QRCodeDetailView.swift b/MyQrCode/Views/QRCodeDetailView.swift index cbc82a2..040ea45 100644 --- a/MyQrCode/Views/QRCodeDetailView.swift +++ b/MyQrCode/Views/QRCodeDetailView.swift @@ -404,9 +404,14 @@ private enum PreviewData { let content = """ BEGIN:VCARD VERSION:3.0 + N:Doe;John;;; FN:John Doe - TEL:+1234567890 - EMAIL:example@example.com + TEL;TYPE=WORK,CELL:(123) 456-7890 + EMAIL;TYPE=PREF,INTERNET:john.doe@example.com + ORG:Example Company + TITLE:Software Engineer + ADR;TYPE=WORK:;;123 Main St;Anytown;CA;12345;USA + URL:https://example.com END:VCARD """.trimmingCharacters(in: .whitespacesAndNewlines) return makeBaseItem(in: context, content: content, qrType: .vcard) diff --git a/docs/QRCODE_DETAIL_VIEW_README.md b/docs/QRCODE_DETAIL_VIEW_README.md index 0e0653f..869bb87 100644 --- a/docs/QRCODE_DETAIL_VIEW_README.md +++ b/docs/QRCODE_DETAIL_VIEW_README.md @@ -57,11 +57,18 @@ SMSTO:<电话号码>:<短信内容> // 联系人信息 (vCard) BEGIN:VCARD VERSION:3.0 +N:Doe;John;;; FN:John Doe -TEL:+1234567890 -EMAIL:example@example.com +TEL;TYPE=WORK,CELL:(123) 456-7890 +EMAIL;TYPE=PREF,INTERNET:john.doe@example.com +ORG:Example Company +TITLE:Software Engineer +ADR;TYPE=WORK:;;123 Main St;Anytown;CA;12345;USA +URL:https://example.com END:VCARD +// 支持vCard 2.1到3.0的自动转换 + // 联系人信息 (MeCard) MECARD:N:<姓名>;TEL:<电话>;EMAIL:<邮箱>;ADR:<地址>;; 示例: MECARD:N:John Doe;TEL:+1234567890;EMAIL:example@example.com;;