Enhance QRCodeParser and CreateQRCodeView to support additional MeCard fields including nickname, birthday, and note; update input handling and display logic in ContactInputView and QRCodeDetailView for improved user experience and data representation.

main
v504 2 months ago
parent 4464897237
commit 6a8d380352

@ -377,28 +377,75 @@ class QRCodeParser {
let components = mecardInfo.components(separatedBy: ";") let components = mecardInfo.components(separatedBy: ";")
var name = "" var name = ""
var nickname = ""
var phone = "" var phone = ""
var email = "" var email = ""
var company = ""
var address = "" var address = ""
var website = ""
var birthday = ""
var note = ""
for component in components { for component in components {
if component.hasPrefix("N:") { let trimmedComponent = component.trimmingCharacters(in: .whitespaces)
name = String(component.dropFirst(2)) if trimmedComponent.isEmpty { continue }
} else if component.hasPrefix("TEL:") {
phone = String(component.dropFirst(4)) if trimmedComponent.hasPrefix("N:") {
} else if component.hasPrefix("EMAIL:") { let nameValue = String(trimmedComponent.dropFirst(2))
email = String(component.dropFirst(6)) let nameParts = nameValue.components(separatedBy: ",")
} else if component.hasPrefix("ADR:") { if nameParts.count >= 2 {
address = String(component.dropFirst(4)) 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))
} }
} }
let title = "联系人信息" var subtitle = ""
let subtitle = "姓名: \(name)\n电话: \(phone)\n邮箱: \(email)\n地址: \(address)" 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( return ParsedQRData(
type: .mecard, type: .mecard,
title: title, title: "联系人信息",
subtitle: subtitle, subtitle: subtitle,
icon: "person.crop.rectangle" icon: "person.crop.rectangle"
) )

@ -10,11 +10,14 @@ struct ContactInputView: View {
@Binding var title: String @Binding var title: String
@Binding var address: String @Binding var address: String
@Binding var website: String @Binding var website: String
@Binding var nickname: String
@Binding var birthday: Date
@Binding var note: String
@FocusState var focusedField: ContactField? @FocusState var focusedField: ContactField?
// //
enum ContactField: Hashable { enum ContactField: Hashable {
case firstName, lastName, phone, email, company, title, address, website case firstName, lastName, phone, email, company, title, address, website, nickname, birthday, note
} }
var body: some View { var body: some View {
@ -52,6 +55,20 @@ struct ContactInputView: View {
} }
} }
//
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("昵称")
.font(.subheadline)
.foregroundColor(.primary)
Spacer()
}
TextField("昵称", text: $nickname)
.textFieldStyle(RoundedBorderTextFieldStyle())
.focused($focusedField, equals: .nickname)
}
// //
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
HStack { HStack {
@ -140,6 +157,34 @@ struct ContactInputView: View {
.autocapitalization(.none) .autocapitalization(.none)
.focused($focusedField, equals: .website) .focused($focusedField, equals: .website)
} }
//
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("生日")
.font(.subheadline)
.foregroundColor(.primary)
Spacer()
}
DatePicker("选择生日", selection: $birthday, displayedComponents: .date)
.datePickerStyle(CompactDatePickerStyle())
.focused($focusedField, equals: .birthday)
}
//
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("备注")
.font(.subheadline)
.foregroundColor(.primary)
Spacer()
}
TextField("备注信息", text: $note)
.textFieldStyle(RoundedBorderTextFieldStyle())
.focused($focusedField, equals: .note)
}
} }
.toolbar { .toolbar {
ToolbarItemGroup(placement: .keyboard) { ToolbarItemGroup(placement: .keyboard) {
@ -163,6 +208,9 @@ struct ContactInputView: View {
company: .constant(""), company: .constant(""),
title: .constant(""), title: .constant(""),
address: .constant(""), address: .constant(""),
website: .constant("") website: .constant(""),
nickname: .constant(""),
birthday: .constant(Date()),
note: .constant("")
) )
} }

@ -26,6 +26,9 @@ struct ContactInputConfig {
let title: Binding<String> let title: Binding<String>
let address: Binding<String> let address: Binding<String>
let website: Binding<String> let website: Binding<String>
let nickname: Binding<String>
let birthday: Binding<Date>
let note: Binding<String>
} }
// MARK: - // MARK: -
@ -104,7 +107,10 @@ struct InputComponentFactory {
company: config.company, company: config.company,
title: config.title, title: config.title,
address: config.address, address: config.address,
website: config.website website: config.website,
nickname: config.nickname,
birthday: config.birthday,
note: config.note
) )
) )
} }

@ -37,6 +37,9 @@ struct CreateQRCodeView: View {
@State private var contactTitle = "" @State private var contactTitle = ""
@State private var contactAddress = "" @State private var contactAddress = ""
@State private var contactWebsite = "" @State private var contactWebsite = ""
@State private var contactNickname = ""
@State private var contactBirthday = Date()
@State private var contactNote = ""
@FocusState private var focusedContactField: ContactInputView.ContactField? @FocusState private var focusedContactField: ContactInputView.ContactField?
// //
@ -173,7 +176,10 @@ struct CreateQRCodeView: View {
company: $contactCompany, company: $contactCompany,
title: $contactTitle, title: $contactTitle,
address: $contactAddress, address: $contactAddress,
website: $contactWebsite website: $contactWebsite,
nickname: $contactNickname,
birthday: $contactBirthday,
note: $contactNote
) )
return InputComponentFactory.createInputComponent( return InputComponentFactory.createInputComponent(
for: selectedQRCodeType, for: selectedQRCodeType,
@ -433,24 +439,55 @@ struct CreateQRCodeView: View {
return vcard return vcard
case .mecard: case .mecard:
var mecard = "MECARD:" var mecard = "MECARD:"
//
if !contactFirstName.isEmpty || !contactLastName.isEmpty { if !contactFirstName.isEmpty || !contactLastName.isEmpty {
mecard += "N:\(contactLastName),\(contactFirstName);" let lastName = contactLastName.isEmpty ? "" : contactLastName
let firstName = contactFirstName.isEmpty ? "" : contactFirstName
mecard += "N:\(lastName),\(firstName);"
}
//
if !contactNickname.isEmpty {
mecard += "NICKNAME:\(contactNickname);"
} }
//
if !contactPhone.isEmpty { if !contactPhone.isEmpty {
mecard += "TEL:\(contactPhone);" mecard += "TEL:\(contactPhone);"
} }
//
if !contactEmail.isEmpty { if !contactEmail.isEmpty {
mecard += "EMAIL:\(contactEmail);" mecard += "EMAIL:\(contactEmail);"
} }
//
if !contactCompany.isEmpty { if !contactCompany.isEmpty {
mecard += "ORG:\(contactCompany);" mecard += "ORG:\(contactCompany);"
} }
//
if !contactAddress.isEmpty { if !contactAddress.isEmpty {
mecard += "ADR:\(contactAddress);" mecard += "ADR:\(contactAddress);"
} }
//
if !contactWebsite.isEmpty { if !contactWebsite.isEmpty {
mecard += "URL:\(contactWebsite);" mecard += "URL:\(contactWebsite);"
} }
//
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd"
let birthdayString = dateFormatter.string(from: contactBirthday)
mecard += "BDAY:\(birthdayString);"
//
if !contactNote.isEmpty {
mecard += "NOTE:\(contactNote);"
}
mecard += ";" mecard += ";"
return mecard return mecard
case .location: case .location:
@ -518,7 +555,35 @@ struct CreateQRCodeView: View {
case .wifi: case .wifi:
historyItem.content = "WiFi: \(wifiSSID) (\(wifiEncryptionType.displayName))" historyItem.content = "WiFi: \(wifiSSID) (\(wifiEncryptionType.displayName))"
case .vcard, .mecard: case .vcard, .mecard:
historyItem.content = "联系人: \(contactFirstName) \(contactLastName)" var contactContent = "联系人: "
if !contactFirstName.isEmpty || !contactLastName.isEmpty {
contactContent += "\(contactFirstName) \(contactLastName)"
}
if !contactNickname.isEmpty {
contactContent += " (\(contactNickname))"
}
if !contactPhone.isEmpty {
contactContent += "\n电话: \(contactPhone)"
}
if !contactEmail.isEmpty {
contactContent += "\n邮箱: \(contactEmail)"
}
if !contactCompany.isEmpty {
contactContent += "\n公司: \(contactCompany)"
}
if !contactTitle.isEmpty {
contactContent += "\n职位: \(contactTitle)"
}
if !contactAddress.isEmpty {
contactContent += "\n地址: \(contactAddress)"
}
if !contactWebsite.isEmpty {
contactContent += "\n网站: \(contactWebsite)"
}
if !contactNote.isEmpty {
contactContent += "\n备注: \(contactNote)"
}
historyItem.content = contactContent
case .location: case .location:
historyItem.content = "位置: \(locationLatitude), \(locationLongitude)" historyItem.content = "位置: \(locationLatitude), \(locationLongitude)"
case .calendar: case .calendar:

@ -362,6 +362,12 @@ struct ShareSheet: UIViewControllerRepresentable {
return NavigationView { QRCodeDetailView(historyItem: item) } return NavigationView { QRCodeDetailView(historyItem: item) }
} }
#Preview("MeCard") {
let ctx = PreviewData.context
let item = PreviewData.mecardSample(in: ctx)
return NavigationView { QRCodeDetailView(historyItem: item) }
}
// MARK: - Preview Data // MARK: - Preview Data
private enum PreviewData { private enum PreviewData {
static let context: NSManagedObjectContext = { static let context: NSManagedObjectContext = {
@ -426,4 +432,9 @@ private enum PreviewData {
let content = "Hello, this is a text message!" let content = "Hello, this is a text message!"
return makeBaseItem(in: context, content: content, qrType: .text) return makeBaseItem(in: context, content: content, qrType: .text)
} }
static func mecardSample(in context: NSManagedObjectContext) -> HistoryItem {
let content = "MECARD:N:Doe,John;NICKNAME:Johnny;TEL:+1234567890;EMAIL:john.doe@example.com;ORG:Example Company;ADR:123 Main St,Anytown,CA,12345,USA;URL:https://example.com;BDAY:19820908;NOTE:Software Engineer;;"
return makeBaseItem(in: context, content: content, qrType: .mecard)
}
} }

@ -70,8 +70,8 @@ END:VCARD
// 支持vCard 2.1到3.0的自动转换 // 支持vCard 2.1到3.0的自动转换
// 联系人信息 (MeCard) // 联系人信息 (MeCard)
MECARD:N:<姓名>;TEL:<电话>;EMAIL:<邮箱>;ADR:<地址>;; MECARD:N:<>,<名>;NICKNAME:<昵称>;TEL:<电话>;EMAIL:<邮箱>;ORG:<公司>;ADR:<地址>;URL:<网站>;BDAY:<生日>;NOTE:<备注>;;
示例: MECARD:N:John Doe;TEL:+1234567890;EMAIL:example@example.com;; 示例: MECARD:N:Doe,John;NICKNAME:Johnny;TEL:+1234567890;EMAIL:john.doe@example.com;ORG:Example Company;ADR:123 Main St,Anytown,CA,12345,USA;URL:https://example.com;BDAY:19820908;NOTE:Software Engineer;;
// 文本内容 // 文本内容
<文本内容> <文本内容>

Loading…
Cancel
Save