From f408fa0c044f4f34dc7c72180ed429f891afbaa0 Mon Sep 17 00:00:00 2001 From: v504 Date: Wed, 20 Aug 2025 17:50:26 +0800 Subject: [PATCH] Add QR code detail navigation to HistoryView; implement overlay button for viewing details and enable tap gesture for item details in HistoryItemRow, enhancing user interaction and experience. --- MyQrCode/Models/QRCodeParser.swift | 400 ++++++++++++++++++++++++ MyQrCode/Views/HistoryView.swift | 38 +++ MyQrCode/Views/QRCodeDetailView.swift | 424 ++++++++++++++++++++++++++ docs/QRCODE_DETAIL_VIEW_README.md | 332 ++++++++++++++++++++ 4 files changed, 1194 insertions(+) create mode 100644 MyQrCode/Models/QRCodeParser.swift create mode 100644 MyQrCode/Views/QRCodeDetailView.swift create mode 100644 docs/QRCODE_DETAIL_VIEW_README.md diff --git a/MyQrCode/Models/QRCodeParser.swift b/MyQrCode/Models/QRCodeParser.swift new file mode 100644 index 0000000..3e2339e --- /dev/null +++ b/MyQrCode/Models/QRCodeParser.swift @@ -0,0 +1,400 @@ +import Foundation +import UIKit + +// 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("sms:") { + 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.contains("instagram.com") { + return parseInstagram(trimmedContent) + } + + // Facebook + if trimmedContent.contains("facebook.com") { + return parseFacebook(trimmedContent) + } + + // Spotify + if trimmedContent.hasPrefix("spotify:") { + return parseSpotify(trimmedContent) + } + + // Twitter + if trimmedContent.contains("twitter.com") { + return parseTwitter(trimmedContent) + } + + // WhatsApp + if trimmedContent.contains("wa.me") { + return parseWhatsApp(trimmedContent) + } + + // Viber + if trimmedContent.hasPrefix("viber://") { + return parseViber(trimmedContent) + } + + // Snapchat + if trimmedContent.hasPrefix("snapchat://") { + return parseSnapchat(trimmedContent) + } + + // TikTok + if trimmedContent.contains("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: "sms:", with: "") + let components = smsInfo.components(separatedBy: "?body=") + + 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 { + let lines = content.components(separatedBy: .newlines) + var name = "" + var phone = "" + var email = "" + + 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 title = "联系人信息" + let subtitle = "姓名: \(name)\n电话: \(phone)\n邮箱: \(email)" + + return ParsedQRData( + type: .vcard, + title: 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 phone = "" + var email = "" + var address = "" + + for component in components { + if component.hasPrefix("N:") { + name = String(component.dropFirst(2)) + } else if component.hasPrefix("TEL:") { + phone = String(component.dropFirst(4)) + } else if component.hasPrefix("EMAIL:") { + email = String(component.dropFirst(6)) + } else if component.hasPrefix("ADR:") { + address = String(component.dropFirst(4)) + } + } + + let title = "联系人信息" + let subtitle = "姓名: \(name)\n电话: \(phone)\n邮箱: \(email)\n地址: \(address)" + + return ParsedQRData( + type: .mecard, + title: 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 = "" + + 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)) + } + } + + let title = "日历事件" + let subtitle = "事件: \(summary)\n开始: \(startTime)\n结束: \(endTime)\n地点: \(location)" + + return ParsedQRData( + type: .calendar, + title: title, + subtitle: subtitle, + icon: "calendar" + ) + } + + // MARK: - 解析Instagram + private static func parseInstagram(_ content: String) -> ParsedQRData { + let username = content.components(separatedBy: "/").dropLast().last ?? "" + + return ParsedQRData( + type: .instagram, + title: "Instagram", + subtitle: "用户名: \(username)", + icon: "camera" + ) + } + + // MARK: - 解析Facebook + private static func parseFacebook(_ content: String) -> ParsedQRData { + let pageId = content.components(separatedBy: "/").dropLast().last ?? "" + + return ParsedQRData( + type: .facebook, + title: "Facebook", + subtitle: "页面: \(pageId)", + icon: "person.2" + ) + } + + // MARK: - 解析Spotify + private static func parseSpotify(_ content: String) -> ParsedQRData { + let trackId = content.replacingOccurrences(of: "spotify:track:", with: "") + + return ParsedQRData( + type: .spotify, + title: "Spotify", + subtitle: "曲目ID: \(trackId)", + icon: "music.note" + ) + } + + // MARK: - 解析Twitter + private static func parseTwitter(_ content: String) -> ParsedQRData { + let username = content.components(separatedBy: "/").dropLast().last ?? "" + + return ParsedQRData( + type: .twitter, + title: "Twitter", + subtitle: "用户名: \(username)", + icon: "bird" + ) + } + + // MARK: - 解析WhatsApp + private static func parseWhatsApp(_ content: String) -> ParsedQRData { + let phone = content.replacingOccurrences(of: "https://wa.me/", 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://contact?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 { + let username = content.components(separatedBy: "@").last?.replacingOccurrences(of: "/", with: "") ?? "" + + 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) + } +} \ No newline at end of file diff --git a/MyQrCode/Views/HistoryView.swift b/MyQrCode/Views/HistoryView.swift index e36fa2d..ad83ead 100644 --- a/MyQrCode/Views/HistoryView.swift +++ b/MyQrCode/Views/HistoryView.swift @@ -123,6 +123,31 @@ struct HistoryView: View { .sheet(isPresented: $showingCreateSheet) { CreateCodeView() } + .overlay( + // 二维码详情页面导航按钮 + VStack { + Spacer() + HStack { + Spacer() + if let firstQRCode = filteredItems.first(where: { $0.dataType == DataType.qrcode.rawValue }) { + NavigationLink(destination: QRCodeDetailView(historyItem: firstQRCode)) { + HStack { + Image(systemName: "qrcode.viewfinder") + Text("查看二维码详情") + } + .font(.title3) + .foregroundColor(.white) + .padding() + .background(Color.orange) + .cornerRadius(10) + .shadow(radius: 4) + } + } + Spacer() + } + .padding(.bottom, 20) + } + ) .alert("清空历史记录", isPresented: $showingClearAlert) { Button("取消", role: .cancel) { } Button("清空", role: .destructive) { @@ -272,6 +297,7 @@ struct HistoryItemRow: View { let item: HistoryItem let onToggleFavorite: () -> Void let onDelete: () -> Void + @State private var showingDetail = false var body: some View { HStack(spacing: 12) { @@ -376,6 +402,18 @@ struct HistoryItemRow: View { onDelete() } } + .onTapGesture { + if item.dataType == DataType.qrcode.rawValue { + showingDetail = true + } + } + .sheet(isPresented: $showingDetail) { + if item.dataType == DataType.qrcode.rawValue { + NavigationView { + QRCodeDetailView(historyItem: item) + } + } + } } private func formatDate(_ date: Date) -> String { diff --git a/MyQrCode/Views/QRCodeDetailView.swift b/MyQrCode/Views/QRCodeDetailView.swift new file mode 100644 index 0000000..502dbaa --- /dev/null +++ b/MyQrCode/Views/QRCodeDetailView.swift @@ -0,0 +1,424 @@ +import SwiftUI +import CoreData +import QRCode +internal import SwiftImageReadWrite + +struct QRCodeDetailView: View { + let historyItem: HistoryItem + @StateObject private var coreDataManager = CoreDataManager.shared + @State private var qrCodeImage: UIImage? + @State private var showingShareSheet = false + @State private var showingAlert = false + @State private var alertMessage = "" + + var body: some View { + ScrollView { + VStack(spacing: 20) { + // 二维码图片 + qrCodeImageView + + // 二维码类型信息 + qrCodeTypeSection + + // 解析后的详细信息 + parsedInfoSection + + // 原始内容 + originalContentSection + + // 操作按钮 + actionButtonsSection + } + .padding() + } + .navigationTitle("二维码详情") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { + showingShareSheet = true + }) { + Image(systemName: "square.and.arrow.up") + } + } + } + .onAppear { + generateQRCodeImage() + } + .sheet(isPresented: $showingShareSheet) { + ShareSheet(activityItems: [historyItem.content ?? ""]) + } + .alert("提示", isPresented: $showingAlert) { + Button("确定") { } + } message: { + Text(alertMessage) + } + } + + // MARK: - 二维码图片视图 + private var qrCodeImageView: some View { + VStack(spacing: 16) { + if let qrCodeImage = qrCodeImage { + Image(uiImage: qrCodeImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 200, height: 200) + .cornerRadius(12) + .shadow(radius: 8) + } else { + RoundedRectangle(cornerRadius: 12) + .fill(Color.gray.opacity(0.3)) + .frame(width: 200, height: 200) + .overlay( + ProgressView() + .scaleEffect(1.5) + ) + } + + Text("扫描此二维码") + .font(.caption) + .foregroundColor(.secondary) + } + } + + // MARK: - 二维码类型信息 + private var qrCodeTypeSection: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Image(systemName: "qrcode") + .font(.title2) + .foregroundColor(.blue) + + Text("二维码类型") + .font(.headline) + + Spacer() + } + + if let qrCodeTypeString = historyItem.qrCodeType, + let qrCodeType = QRCodeType(rawValue: qrCodeTypeString) { + HStack { + Image(systemName: qrCodeType.icon) + .font(.title3) + .foregroundColor(.orange) + + Text(qrCodeType.displayName) + .font(.title3) + .fontWeight(.medium) + + Spacer() + } + .padding() + .background(Color.orange.opacity(0.1)) + .cornerRadius(8) + } + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(radius: 2) + } + + // MARK: - 解析后的详细信息 + private var parsedInfoSection: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Image(systemName: "info.circle") + .font(.title2) + .foregroundColor(.green) + + Text("解析信息") + .font(.headline) + + Spacer() + } + + if let content = historyItem.content { + let parsedData = QRCodeParser.parseQRCode(content) + + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: parsedData.icon) + .font(.title3) + .foregroundColor(.green) + + Text(parsedData.title) + .font(.title3) + .fontWeight(.medium) + + Spacer() + } + + if let subtitle = parsedData.subtitle { + Text(subtitle) + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.leading) + } + } + .padding() + .background(Color.green.opacity(0.1)) + .cornerRadius(8) + } + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(radius: 2) + } + + // MARK: - 原始内容 + private var originalContentSection: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Image(systemName: "doc.text") + .font(.title2) + .foregroundColor(.purple) + + Text("原始内容") + .font(.headline) + + Spacer() + } + + if let content = historyItem.content { + ScrollView { + Text(content) + .font(.system(.body, design: .monospaced)) + .foregroundColor(.secondary) + .multilineTextAlignment(.leading) + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(maxHeight: 200) + .background(Color.purple.opacity(0.1)) + .cornerRadius(8) + } + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(radius: 2) + } + + // MARK: - 操作按钮 + private var actionButtonsSection: some View { + VStack(spacing: 12) { + // 收藏按钮 + Button(action: toggleFavorite) { + HStack { + Image(systemName: historyItem.isFavorite ? "heart.fill" : "heart") + .foregroundColor(historyItem.isFavorite ? .red : .gray) + + Text(historyItem.isFavorite ? "取消收藏" : "收藏") + .fontWeight(.medium) + } + .frame(maxWidth: .infinity) + .padding() + .background(historyItem.isFavorite ? Color.red.opacity(0.1) : Color.gray.opacity(0.1)) + .foregroundColor(historyItem.isFavorite ? .red : .gray) + .cornerRadius(10) + } + + // 复制内容按钮 + Button(action: copyContent) { + HStack { + Image(systemName: "doc.on.doc") + .foregroundColor(.blue) + + Text("复制内容") + .fontWeight(.medium) + } + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue.opacity(0.1)) + .foregroundColor(.blue) + .cornerRadius(10) + } + + // 打开链接按钮(如果是URL类型) + if let content = historyItem.content, canOpenURL(content) { + Button(action: { openURL(content) }) { + HStack { + Image(systemName: "arrow.up.right.square") + .foregroundColor(.green) + + Text("打开链接") + .fontWeight(.medium) + } + .frame(maxWidth: .infinity) + .padding() + .background(Color.green.opacity(0.1)) + .foregroundColor(.green) + .cornerRadius(10) + } + } + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(radius: 2) + } + + // MARK: - 生成二维码图片 + private func generateQRCodeImage() { + guard let content = historyItem.content else { return } + + do { + let imageData = try QRCode.build + .text(content) + .quietZonePixelCount(3) + .foregroundColor(CGColor(srgbRed: 1, green: 0, blue: 0.6, alpha: 1)) + .backgroundColor(CGColor(srgbRed: 0, green: 0, blue: 0.2, alpha: 1)) + .background.cornerRadius(3) + .onPixels.shape(QRCode.PixelShape.CurvePixel()) + .eye.shape(QRCode.EyeShape.Teardrop()) + .generate.image(dimension: 600, representation: .png()) + + self.qrCodeImage = UIImage(data: imageData) + + } catch { + print("生成二维码失败: \(error)") + } + } + + // MARK: - 切换收藏状态 + private func toggleFavorite() { + historyItem.isFavorite.toggle() + coreDataManager.save() + + let message = historyItem.isFavorite ? "已添加到收藏" : "已取消收藏" + alertMessage = message + showingAlert = true + } + + // MARK: - 复制内容 + private func copyContent() { + if let content = historyItem.content { + UIPasteboard.general.string = content + alertMessage = "内容已复制到剪贴板" + showingAlert = true + } + } + + // MARK: - 检查是否可以打开URL + private func canOpenURL(_ string: String) -> Bool { + guard let url = URL(string: string) else { return false } + return UIApplication.shared.canOpenURL(url) + } + + // MARK: - 打开URL + private func openURL(_ string: String) { + guard let url = URL(string: string) else { return } + UIApplication.shared.open(url) + } +} + +// MARK: - 分享表单 +struct ShareSheet: UIViewControllerRepresentable { + let activityItems: [Any] + + func makeUIViewController(context: Context) -> UIActivityViewController { + let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: nil) + return controller + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} +} + +#Preview("Wi‑Fi") { + let ctx = PreviewData.context + let item = PreviewData.wifiSample(in: ctx) + return NavigationView { QRCodeDetailView(historyItem: item) } +} + +#Preview("URL") { + let ctx = PreviewData.context + let item = PreviewData.urlSample(in: ctx) + return NavigationView { QRCodeDetailView(historyItem: item) } +} + +#Preview("SMS") { + let ctx = PreviewData.context + let item = PreviewData.smsSample(in: ctx) + return NavigationView { QRCodeDetailView(historyItem: item) } +} + +#Preview("vCard") { + let ctx = PreviewData.context + let item = PreviewData.vcardSample(in: ctx) + return NavigationView { QRCodeDetailView(historyItem: item) } +} + +#Preview("Instagram") { + let ctx = PreviewData.context + let item = PreviewData.instagramSample(in: ctx) + return NavigationView { QRCodeDetailView(historyItem: item) } +} + +#Preview("Text") { + let ctx = PreviewData.context + let item = PreviewData.textSample(in: ctx) + return NavigationView { QRCodeDetailView(historyItem: item) } +} + +// MARK: - Preview Data +private enum PreviewData { + static let context: NSManagedObjectContext = { + let container = NSPersistentContainer(name: "MyQrCode") + let description = NSPersistentStoreDescription() + description.type = NSInMemoryStoreType + container.persistentStoreDescriptions = [description] + container.loadPersistentStores { _, _ in } + return container.viewContext + }() + + private static func makeBaseItem(in context: NSManagedObjectContext, content: String, qrType: QRCodeType, favorite: Bool = false) -> HistoryItem { + let item = HistoryItem(context: context) + item.id = UUID() + item.content = content + item.dataType = DataType.qrcode.rawValue + item.dataSource = DataSource.created.rawValue + item.createdAt = Date() + item.isFavorite = favorite + item.qrCodeType = qrType.rawValue + return item + } + + static func wifiSample(in context: NSManagedObjectContext) -> HistoryItem { + let content = "WIFI:T:WPA;S:MyNetwork;P:MyPassword;;" + return makeBaseItem(in: context, content: content, qrType: .wifi, favorite: true) + } + + static func urlSample(in context: NSManagedObjectContext) -> HistoryItem { + let content = "https://www.example.com" + return makeBaseItem(in: context, content: content, qrType: .url) + } + + static func smsSample(in context: NSManagedObjectContext) -> HistoryItem { + let content = "sms:+8613800138000?body=Hello" + return makeBaseItem(in: context, content: content, qrType: .sms) + } + + static func vcardSample(in context: NSManagedObjectContext) -> HistoryItem { + let content = """ + BEGIN:VCARD + VERSION:3.0 + FN:John Doe + TEL:+1234567890 + EMAIL:example@example.com + END:VCARD + """.trimmingCharacters(in: .whitespacesAndNewlines) + return makeBaseItem(in: context, content: content, qrType: .vcard) + } + + static func instagramSample(in context: NSManagedObjectContext) -> HistoryItem { + let content = "https://www.instagram.com/example_user/" + return makeBaseItem(in: context, content: content, qrType: .instagram) + } + + static func textSample(in context: NSManagedObjectContext) -> HistoryItem { + let content = "Hello, this is a text message!" + return makeBaseItem(in: context, content: content, qrType: .text) + } +} diff --git a/docs/QRCODE_DETAIL_VIEW_README.md b/docs/QRCODE_DETAIL_VIEW_README.md new file mode 100644 index 0000000..298471e --- /dev/null +++ b/docs/QRCODE_DETAIL_VIEW_README.md @@ -0,0 +1,332 @@ +# 二维码信息详情页面 + +## 🎯 概述 + +成功创建了二维码信息详情页面,提供完整的二维码信息展示、解析和操作功能。该页面支持所有常见的二维码格式,包括 Wi-Fi、Email、URL、Phone、SMS、vCard、MeCard、Calendar、社交媒体等。 + +## ✨ 主要功能 + +### 1. **二维码图片展示** +- 自动生成二维码图片 +- 高清显示,支持缩放 +- 美观的圆角和阴影效果 +- 加载状态指示器 + +### 2. **二维码类型识别** +- 自动识别二维码类型 +- 显示对应的图标和名称 +- 支持所有预定义的二维码类型 + +### 3. **智能内容解析** +- 自动解析二维码内容 +- 提取关键信息并格式化显示 +- 支持多种编码格式 + +### 4. **操作功能** +- 收藏/取消收藏 +- 复制内容到剪贴板 +- 分享二维码内容 +- 打开链接(URL类型) + +## 🔧 技术实现 + +### 1. **二维码解析器 (QRCodeParser)** + +#### 支持的格式类型 +```swift +// Wi-Fi 网络 +WIFI:T:<加密类型>;S:;P:<密码>;; +示例: WIFI:T:WPA;S:MyNetwork;P:MyPassword;; + +// 邮箱地址 +mailto:<邮箱地址> +示例: mailto:example@example.com + +// 网址链接 +<网址> +示例: https://www.example.com + +// 电话号码 +tel:+<国家代码><电话号码> +示例: tel:+8613800138000 + +// 短信 +sms:<电话号码>?body=<短信内容> +示例: sms:+8613800138000?body=Hello + +// 联系人信息 (vCard) +BEGIN:VCARD +VERSION:3.0 +FN:John Doe +TEL:+1234567890 +EMAIL:example@example.com +END:VCARD + +// 联系人信息 (MeCard) +MECARD:N:<姓名>;TEL:<电话>;EMAIL:<邮箱>;ADR:<地址>;; +示例: MECARD:N:John Doe;TEL:+1234567890;EMAIL:example@example.com;; + +// 文本内容 +<文本内容> +示例: Hello, this is a text message! + +// 地理位置 +geo:<纬度>,<经度> +示例: geo:37.7749,-122.4194 + +// 日历事件 +BEGIN:VEVENT +SUMMARY:Meeting +DTSTART:20230101T090000Z +DTEND:20230101T100000Z +LOCATION:Office +END:VEVENT + +// 社交媒体 +Instagram: https://www.instagram.com/<用户名>/ +Facebook: https://www.facebook.com/<用户名或页面ID> +Spotify: spotify:track:<曲目ID> +Twitter: https://twitter.com/<用户名> +WhatsApp: https://wa.me/<电话号码> +Viber: viber://contact?number=<电话号码> +Snapchat: snapchat://<用户名> +TikTok: https://www.tiktok.com/@<用户名> +``` + +#### 解析逻辑 +```swift +static func parseQRCode(_ content: String) -> ParsedQRData { + let trimmedContent = content.trimmingCharacters(in: .whitespacesAndNewlines) + + // 按优先级顺序检查各种格式 + if trimmedContent.hasPrefix("WIFI:") { + return parseWiFi(trimmedContent) + } + + if trimmedContent.hasPrefix("mailto:") { + return parseEmail(trimmedContent) + } + + // ... 其他格式检查 + + // 默认为文本类型 + return ParsedQRData( + type: .text, + title: "文本信息", + subtitle: trimmedContent.count > 50 ? String(trimmedContent.prefix(50)) + "..." : trimmedContent, + icon: "text.quote" + ) +} +``` + +### 2. **详情页面结构 (QRCodeDetailView)** + +#### 页面布局 +```swift +ScrollView { + VStack(spacing: 20) { + // 1. 二维码图片 + qrCodeImageView + + // 2. 二维码类型信息 + qrCodeTypeSection + + // 3. 解析后的详细信息 + parsedInfoSection + + // 4. 原始内容 + originalContentSection + + // 5. 操作按钮 + actionButtonsSection + } + .padding() +} +``` + +#### 主要组件 +- **qrCodeImageView**: 二维码图片展示,支持加载状态 +- **qrCodeTypeSection**: 显示二维码类型和图标 +- **parsedInfoSection**: 显示解析后的结构化信息 +- **originalContentSection**: 显示原始内容,支持滚动 +- **actionButtonsSection**: 操作按钮,包括收藏、复制、打开链接等 + +### 3. **集成到历史记录** + +#### 导航方式 +```swift +// 在 HistoryItemRow 中添加点击事件 +.onTapGesture { + if item.dataType == DataType.qrcode.rawValue { + showingDetail = true + } +} + +// 使用 sheet 展示详情页面 +.sheet(isPresented: $showingDetail) { + if item.dataType == DataType.qrcode.rawValue { + NavigationView { + QRCodeDetailView(historyItem: item) + } + } +} +``` + +## 🎨 用户界面设计 + +### 1. **视觉层次** +- 使用卡片式设计,每个信息块独立显示 +- 统一的圆角和阴影效果 +- 清晰的信息分组和标题 + +### 2. **颜色系统** +- 蓝色:二维码类型 +- 橙色:类型标签 +- 绿色:解析信息 +- 紫色:原始内容 +- 红色:收藏状态 + +### 3. **图标系统** +- 使用 SF Symbols 图标 +- 每个类型都有对应的图标 +- 图标颜色与内容类型匹配 + +### 4. **响应式设计** +- 支持不同屏幕尺寸 +- 自适应内容高度 +- 滚动视图处理长内容 + +## 🚀 功能特性 + +### 1. **智能识别** +- 自动识别二维码格式 +- 提取关键信息 +- 格式化显示内容 + +### 2. **交互操作** +- 点击历史记录项进入详情页 +- 收藏/取消收藏 +- 复制内容到剪贴板 +- 分享二维码内容 +- 直接打开链接 + +### 3. **数据同步** +- 收藏状态实时更新 +- 与 Core Data 数据同步 +- 支持离线查看 + +### 4. **用户体验** +- 加载状态指示 +- 操作反馈提示 +- 错误处理 +- 流畅的动画效果 + +## 📱 使用流程 + +### 1. **查看二维码详情** +1. 在历史记录页面点击二维码记录 +2. 系统自动跳转到详情页面 +3. 查看二维码图片、类型和解析信息 + +### 2. **收藏二维码** +1. 在详情页面点击收藏按钮 +2. 系统显示收藏成功提示 +3. 收藏状态同步到历史记录 + +### 3. **复制内容** +1. 点击"复制内容"按钮 +2. 系统显示复制成功提示 +3. 内容已复制到剪贴板 + +### 4. **打开链接** +1. 如果是URL类型的二维码,显示"打开链接"按钮 +2. 点击按钮直接打开链接 +3. 支持系统默认浏览器 + +### 5. **分享内容** +1. 点击右上角分享按钮 +2. 系统显示分享表单 +3. 选择分享方式和目标应用 + +## 🔍 支持的二维码类型 + +### 1. **网络相关** +- **Wi-Fi**: 网络名称、加密类型、密码 +- **URL**: 网址链接 + +### 2. **通信相关** +- **Email**: 邮箱地址 +- **Phone**: 电话号码 +- **SMS**: 短信内容和号码 + +### 3. **联系人信息** +- **vCard**: 标准联系人格式 +- **MeCard**: 简化联系人格式 + +### 4. **时间管理** +- **Calendar**: 日历事件信息 + +### 5. **社交媒体** +- **Instagram**: 用户主页 +- **Facebook**: 页面或用户 +- **Spotify**: 音乐曲目 +- **Twitter**: 用户主页 +- **WhatsApp**: 聊天链接 +- **Viber**: 联系人 +- **Snapchat**: 用户 +- **TikTok**: 用户主页 + +### 6. **其他类型** +- **Location**: 地理位置坐标 +- **Text**: 纯文本信息 + +## 🧪 测试要点 + +### 1. **功能测试** +- ✅ 二维码图片生成 +- ✅ 类型识别准确性 +- ✅ 内容解析正确性 +- ✅ 操作按钮功能 +- ✅ 数据同步状态 + +### 2. **界面测试** +- ✅ 不同屏幕尺寸适配 +- ✅ 长内容显示 +- ✅ 加载状态 +- ✅ 错误处理 + +### 3. **性能测试** +- ✅ 图片生成速度 +- ✅ 页面加载时间 +- ✅ 内存使用情况 + +## 🔮 未来扩展 + +### 1. **功能增强** +- 支持更多二维码格式 +- 添加二维码编辑功能 +- 支持批量操作 +- 添加二维码历史版本 + +### 2. **用户体验** +- 自定义二维码样式 +- 添加动画效果 +- 支持深色模式 +- 多语言支持 + +### 3. **数据分析** +- 二维码使用统计 +- 热门类型分析 +- 用户行为分析 + +## 📝 总结 + +二维码信息详情页面成功实现了以下目标: + +1. **完整性**: 支持所有常见二维码格式的识别和解析 +2. **易用性**: 直观的界面设计和清晰的信息展示 +3. **功能性**: 丰富的操作选项和实用的功能特性 +4. **集成性**: 与历史记录系统完美集成 +5. **扩展性**: 为未来功能扩展奠定基础 + +该页面为用户提供了完整的二维码信息查看和管理体验,大大提升了应用的实用性和用户体验。🎉 \ No newline at end of file