From 12764a259823672e9774e8c2915726a65c6bf262 Mon Sep 17 00:00:00 2001 From: v504 Date: Fri, 22 Aug 2025 15:20:26 +0800 Subject: [PATCH] Add DESCRIPTION field and time formatting to QRCodeParser for enhanced calendar event parsing --- MyQrCode/Models/QRCodeParser.swift | 32 +- .../Views/Components/CalendarInputView.swift | 150 ++++ MyQrCode/Views/Components/CardView.swift | 325 +++++++++ .../Views/Components/ContactInputView.swift | 168 +++++ .../Views/Components/DatePickerView.swift | 141 ++++ .../Views/Components/EmailInputView.swift | 161 +++++ MyQrCode/Views/Components/FormView.swift | 285 ++++++++ .../Components/InputComponentFactory.swift | 217 ++++++ .../Views/Components/InputFieldView.swift | 208 ++++++ MyQrCode/Views/Components/InputHintView.swift | 70 ++ .../Views/Components/InputTitleView.swift | 58 ++ .../Components/KeyboardToolbarView.swift | 273 ++++++++ MyQrCode/Views/Components/ListView.swift | 318 +++++++++ .../Views/Components/LocationInputView.swift | 111 +++ .../Views/Components/PhoneInputView.swift | 163 +++++ MyQrCode/Views/Components/PickerView.swift | 108 +++ .../Views/Components/QRCodePreviewView.swift | 90 +++ .../Views/Components/SocialInputView.swift | 199 ++++++ .../Views/Components/TextEditorView.swift | 200 ++++++ MyQrCode/Views/Components/TextInputView.swift | 110 +++ MyQrCode/Views/Components/URLInputView.swift | 102 +++ .../Views/Components/UtilityFunctions.swift | 390 +++++++++++ .../Views/Components/ValidationView.swift | 237 +++++++ MyQrCode/Views/Components/WiFiInputView.swift | 128 ++++ MyQrCode/Views/CreateQRCodeView.swift.backup | 642 ------------------ 25 files changed, 4243 insertions(+), 643 deletions(-) create mode 100644 MyQrCode/Views/Components/CalendarInputView.swift create mode 100644 MyQrCode/Views/Components/CardView.swift create mode 100644 MyQrCode/Views/Components/ContactInputView.swift create mode 100644 MyQrCode/Views/Components/DatePickerView.swift create mode 100644 MyQrCode/Views/Components/EmailInputView.swift create mode 100644 MyQrCode/Views/Components/FormView.swift create mode 100644 MyQrCode/Views/Components/InputComponentFactory.swift create mode 100644 MyQrCode/Views/Components/InputFieldView.swift create mode 100644 MyQrCode/Views/Components/InputHintView.swift create mode 100644 MyQrCode/Views/Components/InputTitleView.swift create mode 100644 MyQrCode/Views/Components/KeyboardToolbarView.swift create mode 100644 MyQrCode/Views/Components/ListView.swift create mode 100644 MyQrCode/Views/Components/LocationInputView.swift create mode 100644 MyQrCode/Views/Components/PhoneInputView.swift create mode 100644 MyQrCode/Views/Components/PickerView.swift create mode 100644 MyQrCode/Views/Components/QRCodePreviewView.swift create mode 100644 MyQrCode/Views/Components/SocialInputView.swift create mode 100644 MyQrCode/Views/Components/TextEditorView.swift create mode 100644 MyQrCode/Views/Components/TextInputView.swift create mode 100644 MyQrCode/Views/Components/URLInputView.swift create mode 100644 MyQrCode/Views/Components/UtilityFunctions.swift create mode 100644 MyQrCode/Views/Components/ValidationView.swift create mode 100644 MyQrCode/Views/Components/WiFiInputView.swift delete mode 100644 MyQrCode/Views/CreateQRCodeView.swift.backup diff --git a/MyQrCode/Models/QRCodeParser.swift b/MyQrCode/Models/QRCodeParser.swift index 3e2339e..a69f2d8 100644 --- a/MyQrCode/Models/QRCodeParser.swift +++ b/MyQrCode/Models/QRCodeParser.swift @@ -243,6 +243,7 @@ class QRCodeParser { var startTime = "" var endTime = "" var location = "" + var description = "" for line in lines { if line.hasPrefix("SUMMARY:") { @@ -253,11 +254,23 @@ class QRCodeParser { 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 = "日历事件" - let subtitle = "事件: \(summary)\n开始: \(startTime)\n结束: \(endTime)\n地点: \(location)" + var subtitle = "事件: \(summary)\n开始: \(formattedStartTime)\n结束: \(formattedEndTime)" + if !location.isEmpty { + subtitle += "\n地点: \(location)" + } + if !description.isEmpty { + subtitle += "\n描述: \(description)" + } return ParsedQRData( type: .calendar, @@ -267,6 +280,23 @@ class QRCodeParser { ) } + // 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.components(separatedBy: "/").dropLast().last ?? "" diff --git a/MyQrCode/Views/Components/CalendarInputView.swift b/MyQrCode/Views/Components/CalendarInputView.swift new file mode 100644 index 0000000..a8b9e0c --- /dev/null +++ b/MyQrCode/Views/Components/CalendarInputView.swift @@ -0,0 +1,150 @@ +import SwiftUI + +// MARK: - 日历事件输入组件 +struct CalendarInputView: View { + @Binding var eventTitle: String + @Binding var eventDescription: String + @Binding var startDate: Date + @Binding var endDate: Date + @Binding var location: String + @FocusState var focusedField: CalendarField? + + // 日历字段枚举 + enum CalendarField: Hashable { + case title, description, location + } + + var body: some View { + VStack(spacing: 16) { + // 事件标题 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("事件标题") + .font(.subheadline) + .foregroundColor(.primary) + Text("*") + .foregroundColor(.red) + Spacer() + } + + TextField("会议标题", text: $eventTitle) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .focused($focusedField, equals: .title) + } + + // 事件描述 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("事件描述") + .font(.subheadline) + .foregroundColor(.primary) + Spacer() + } + + TextField("事件详细描述", text: $eventDescription) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .focused($focusedField, equals: .description) + } + + // 开始时间 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("开始时间") + .font(.subheadline) + .foregroundColor(.primary) + Text("*") + .foregroundColor(.red) + Spacer() + } + + DatePicker("开始时间", selection: $startDate, displayedComponents: [.date, .hourAndMinute]) + .datePickerStyle(CompactDatePickerStyle()) + .labelsHidden() + } + + // 结束时间 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("结束时间") + .font(.subheadline) + .foregroundColor(.primary) + Text("*") + .foregroundColor(.red) + Spacer() + } + + DatePicker("结束时间", selection: $endDate, displayedComponents: [.date, .hourAndMinute]) + .datePickerStyle(CompactDatePickerStyle()) + .labelsHidden() + } + + // 地点 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("地点") + .font(.subheadline) + .foregroundColor(.primary) + Spacer() + } + + TextField("会议地点", text: $location) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .focused($focusedField, equals: .location) + } + + // 时间验证提示 + if endDate <= startDate { + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: "exclamationmark.triangle") + .font(.caption) + .foregroundColor(.orange) + + Text("时间设置提示") + .font(.caption) + .foregroundColor(.primary) + + Spacer() + } + + Text("结束时间必须晚于开始时间") + .font(.caption) + .foregroundColor(.orange) + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color.orange.opacity(0.1)) + ) + } + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("完成") { + focusedField = nil + } + .foregroundColor(.blue) + .font(.system(size: 16, weight: .medium)) + } + } + .onAppear { + // 设置默认时间 + if startDate == Date() { + startDate = Date() + endDate = Calendar.current.date(byAdding: .hour, value: 1, to: startDate) ?? startDate + } + } + } +} + +#Preview { + CalendarInputView( + eventTitle: .constant(""), + eventDescription: .constant(""), + startDate: .constant(Date()), + endDate: .constant(Calendar.current.date(byAdding: .hour, value: 1, to: Date()) ?? Date()), + location: .constant("") + ) +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/CardView.swift b/MyQrCode/Views/Components/CardView.swift new file mode 100644 index 0000000..57944f4 --- /dev/null +++ b/MyQrCode/Views/Components/CardView.swift @@ -0,0 +1,325 @@ +import SwiftUI + +// MARK: - 简化的卡片组件 +struct SimpleCardView: View { + let content: AnyView + let padding: EdgeInsets + let cornerRadius: CGFloat + let shadowColor: Color + let shadowRadius: CGFloat + let shadowOffset: CGSize + let backgroundColor: Color + + init( + padding: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16), + cornerRadius: CGFloat = 12, + shadowColor: Color = .black.opacity(0.1), + shadowRadius: CGFloat = 8, + shadowOffset: CGSize = CGSize(width: 0, height: 4), + backgroundColor: Color = Color(.systemBackground), + @ViewBuilder content: () -> some View + ) { + self.padding = padding + self.cornerRadius = cornerRadius + self.shadowColor = shadowColor + self.shadowRadius = shadowRadius + self.shadowOffset = shadowOffset + self.backgroundColor = backgroundColor + self.content = AnyView(content()) + } + + var body: some View { + content + .padding(padding) + .background(backgroundColor) + .cornerRadius(cornerRadius) + .shadow( + color: shadowColor, + radius: shadowRadius, + x: shadowOffset.width, + y: shadowOffset.height + ) + } +} + +// MARK: - 预定义的卡片样式 +extension SimpleCardView { + static func standard( + @ViewBuilder content: () -> Content + ) -> SimpleCardView { + SimpleCardView(content: content) + } + + static func elevated( + @ViewBuilder content: () -> Content + ) -> SimpleCardView { + SimpleCardView( + shadowColor: .black.opacity(0.15), + shadowRadius: 12, + shadowOffset: CGSize(width: 0, height: 6), + content: content + ) + } + + static func subtle( + @ViewBuilder content: () -> Content + ) -> SimpleCardView { + SimpleCardView( + shadowColor: .black.opacity(0.05), + shadowRadius: 4, + shadowOffset: CGSize(width: 0, height: 2), + content: content + ) + } + + static func compact( + @ViewBuilder content: () -> Content + ) -> SimpleCardView { + SimpleCardView( + padding: EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12), + cornerRadius: 8, + content: content + ) + } + + static func large( + @ViewBuilder content: () -> Content + ) -> SimpleCardView { + SimpleCardView( + padding: EdgeInsets(top: 24, leading: 24, bottom: 24, trailing: 24), + cornerRadius: 16, + content: content + ) + } +} + +// MARK: - 信息卡片组件 +struct InfoCard: View { + let title: String + let subtitle: String? + let icon: String? + let iconColor: Color + let content: String + let actionTitle: String? + let action: (() -> Void)? + + init( + title: String, + subtitle: String? = nil, + icon: String? = nil, + iconColor: Color = .blue, + content: String, + actionTitle: String? = nil, + action: (() -> Void)? = nil + ) { + self.title = title + self.subtitle = subtitle + self.icon = icon + self.iconColor = iconColor + self.content = content + self.actionTitle = actionTitle + self.action = action + } + + var body: some View { + SimpleCardView.standard { + VStack(alignment: .leading, spacing: 12) { + // 标题行 + HStack { + if let icon = icon { + Image(systemName: icon) + .font(.title2) + .foregroundColor(iconColor) + } + + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(.headline) + .foregroundColor(.primary) + + if let subtitle = subtitle { + Text(subtitle) + .font(.caption) + .foregroundColor(.secondary) + } + } + + Spacer() + } + + // 内容 + Text(content) + .font(.body) + .foregroundColor(.primary) + .lineLimit(nil) + + // 操作按钮 + if let actionTitle = actionTitle, let action = action { + Button(action: action) { + Text(actionTitle) + .font(.subheadline) + .fontWeight(.medium) + .foregroundColor(iconColor) + } + } + } + } + } +} + +// MARK: - 统计卡片组件 +struct StatCard: View { + let title: String + let value: String + let subtitle: String? + let icon: String? + let iconColor: Color + let trend: Trend? + + enum Trend { + case up(String) + case down(String) + case neutral(String) + } + + init( + title: String, + value: String, + subtitle: String? = nil, + icon: String? = nil, + iconColor: Color = .blue, + trend: Trend? = nil + ) { + self.title = title + self.value = value + self.subtitle = subtitle + self.icon = icon + self.iconColor = iconColor + self.trend = trend + } + + var body: some View { + SimpleCardView.standard { + VStack(alignment: .leading, spacing: 12) { + // 标题和图标 + HStack { + if let icon = icon { + Image(systemName: icon) + .font(.title2) + .foregroundColor(iconColor) + } + + Text(title) + .font(.subheadline) + .foregroundColor(.secondary) + + Spacer() + } + + // 数值 + Text(value) + .font(.title) + .fontWeight(.bold) + .foregroundColor(.primary) + + // 副标题和趋势 + HStack { + if let subtitle = subtitle { + Text(subtitle) + .font(.caption) + .foregroundColor(.secondary) + } + + Spacer() + + if let trend = trend { + HStack(spacing: 4) { + Image(systemName: trendIcon) + .font(.caption) + .foregroundColor(trendColor) + + Text(trendValue) + .font(.caption) + .foregroundColor(trendColor) + } + } + } + } + } + } + + private var trendIcon: String { + switch trend { + case .up: + return "arrow.up" + case .down: + return "arrow.down" + case .neutral: + return "minus" + case .none: + return "" + } + } + + private var trendColor: Color { + switch trend { + case .up: + return .green + case .down: + return .red + case .neutral: + return .orange + case .none: + return .clear + } + } + + private var trendValue: String { + switch trend { + case .up(let value), .down(let value), .neutral(let value): + return value + case .none: + return "" + } + } +} + +#Preview { + VStack(spacing: 20) { + // 标准卡片 + SimpleCardView.standard { + VStack(alignment: .leading, spacing: 12) { + Text("标准卡片") + .font(.headline) + Text("这是一个标准样式的卡片组件") + .font(.body) + } + } + + // 信息卡片 + InfoCard( + title: "提示信息", + subtitle: "重要提醒", + icon: "info.circle", + content: "这是一个信息卡片,用于显示重要的提示信息。", + actionTitle: "了解更多", + action: {} + ) + + // 统计卡片 + StatCard( + title: "总用户数", + value: "1,234", + subtitle: "本月新增 123", + icon: "person.3", + trend: .up("+12%") + ) + + // 紧凑卡片 + SimpleCardView.compact { + Text("紧凑卡片") + .font(.headline) + } + } + .padding() +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/ContactInputView.swift b/MyQrCode/Views/Components/ContactInputView.swift new file mode 100644 index 0000000..9c1c3a9 --- /dev/null +++ b/MyQrCode/Views/Components/ContactInputView.swift @@ -0,0 +1,168 @@ +import SwiftUI + +// MARK: - 联系人信息输入组件 +struct ContactInputView: View { + @Binding var firstName: String + @Binding var lastName: String + @Binding var phone: String + @Binding var email: String + @Binding var company: String + @Binding var title: String + @Binding var address: String + @Binding var website: String + @FocusState var focusedField: ContactField? + + // 联系人字段枚举 + enum ContactField: Hashable { + case firstName, lastName, phone, email, company, title, address, website + } + + var body: some View { + VStack(spacing: 16) { + // 姓名 + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("名") + .font(.subheadline) + .foregroundColor(.primary) + Text("*") + .foregroundColor(.red) + Spacer() + } + + TextField("名", text: $firstName) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .focused($focusedField, equals: .firstName) + } + + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("姓") + .font(.subheadline) + .foregroundColor(.primary) + Text("*") + .foregroundColor(.red) + Spacer() + } + + TextField("姓", text: $lastName) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .focused($focusedField, equals: .lastName) + } + } + + // 电话 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("电话") + .font(.subheadline) + .foregroundColor(.primary) + Spacer() + } + + TextField("+86 138 0013 8000", text: $phone) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .keyboardType(.phonePad) + .focused($focusedField, equals: .phone) + } + + // 邮箱 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("邮箱") + .font(.subheadline) + .foregroundColor(.primary) + Spacer() + } + + TextField("user@example.com", text: $email) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .keyboardType(.emailAddress) + .autocapitalization(.none) + .focused($focusedField, equals: .email) + } + + // 公司 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("公司") + .font(.subheadline) + .foregroundColor(.primary) + Spacer() + } + + TextField("公司名称", text: $company) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .focused($focusedField, equals: .company) + } + + // 职位 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("职位") + .font(.subheadline) + .foregroundColor(.primary) + Spacer() + } + + TextField("职位名称", text: $title) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .focused($focusedField, equals: .title) + } + + // 地址 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("地址") + .font(.subheadline) + .foregroundColor(.primary) + Spacer() + } + + TextField("详细地址", text: $address) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .focused($focusedField, equals: .address) + } + + // 网站 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("网站") + .font(.subheadline) + .foregroundColor(.primary) + Spacer() + } + + TextField("https://example.com", text: $website) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .keyboardType(.URL) + .autocapitalization(.none) + .focused($focusedField, equals: .website) + } + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("完成") { + focusedField = nil + } + .foregroundColor(.blue) + .font(.system(size: 16, weight: .medium)) + } + } + } +} + +#Preview { + ContactInputView( + firstName: .constant(""), + lastName: .constant(""), + phone: .constant(""), + email: .constant(""), + company: .constant(""), + title: .constant(""), + address: .constant(""), + website: .constant("") + ) +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/DatePickerView.swift b/MyQrCode/Views/Components/DatePickerView.swift new file mode 100644 index 0000000..e68a8a9 --- /dev/null +++ b/MyQrCode/Views/Components/DatePickerView.swift @@ -0,0 +1,141 @@ +import SwiftUI + +// MARK: - 通用日期选择器组件 +struct DatePickerView: View { + let title: String + let isRequired: Bool + let date: Binding + let displayedComponents: DatePickerComponents + let icon: String? + + init( + title: String, + isRequired: Bool = false, + date: Binding, + displayedComponents: DatePickerComponents = [.date, .hourAndMinute], + icon: String? = nil + ) { + self.title = title + self.isRequired = isRequired + self.date = date + self.displayedComponents = displayedComponents + self.icon = icon + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + InputTitleView.required(title, icon: icon) + + DatePicker(title, selection: date, displayedComponents: displayedComponents) + .datePickerStyle(CompactDatePickerStyle()) + .labelsHidden() + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color(.systemGray6)) + ) + } + } +} + +// MARK: - 预定义的日期选择器样式 +extension DatePickerView { + static func dateOnly( + title: String, + isRequired: Bool = false, + date: Binding, + icon: String? = nil + ) -> DatePickerView { + DatePickerView( + title: title, + isRequired: isRequired, + date: date, + displayedComponents: .date, + icon: icon + ) + } + + static func timeOnly( + title: String, + isRequired: Bool = false, + date: Binding, + icon: String? = nil + ) -> DatePickerView { + DatePickerView( + title: title, + isRequired: isRequired, + date: date, + displayedComponents: .hourAndMinute, + icon: icon + ) + } + + static func dateAndTime( + title: String, + isRequired: Bool = false, + date: Binding, + icon: String? = nil + ) -> DatePickerView { + DatePickerView( + title: title, + isRequired: isRequired, + date: date, + displayedComponents: [.date, .hourAndMinute], + icon: icon + ) + } + + static func startDate( + title: String = "开始时间", + isRequired: Bool = true, + date: Binding, + icon: String? = "calendar" + ) -> DatePickerView { + DatePickerView( + title: title, + isRequired: isRequired, + date: date, + displayedComponents: [.date, .hourAndMinute], + icon: icon + ) + } + + static func endDate( + title: String = "结束时间", + isRequired: Bool = true, + date: Binding, + icon: String? = "calendar" + ) -> DatePickerView { + DatePickerView( + title: title, + isRequired: isRequired, + date: date, + displayedComponents: [.date, .hourAndMinute], + icon: icon + ) + } +} + +#Preview { + VStack(spacing: 16) { + DatePickerView.dateOnly( + title: "选择日期", + date: .constant(Date()) + ) + + DatePickerView.timeOnly( + title: "选择时间", + date: .constant(Date()) + ) + + DatePickerView.dateAndTime( + title: "选择日期和时间", + date: .constant(Date()) + ) + + DatePickerView.startDate(date: .constant(Date())) + DatePickerView.endDate(date: .constant(Date().addingTimeInterval(3600))) + } + .padding() +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/EmailInputView.swift b/MyQrCode/Views/Components/EmailInputView.swift new file mode 100644 index 0000000..17a88e1 --- /dev/null +++ b/MyQrCode/Views/Components/EmailInputView.swift @@ -0,0 +1,161 @@ +import SwiftUI + +// MARK: - Email输入组件 +struct EmailInputView: View { + @Binding var emailAddress: String + @Binding var emailSubject: String + @Binding var emailBody: String + @Binding var emailCc: String + @Binding var emailBcc: String + @FocusState var focusedEmailField: EmailField? + + // Email字段枚举 + enum EmailField: Hashable { + case address, subject, body, cc, bcc + } + + var body: some View { + VStack(spacing: 16) { + // Email地址 (必填) + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("邮箱地址") + .font(.subheadline) + .foregroundColor(.primary) + Text("*") + .foregroundColor(.red) + Spacer() + } + + TextField("user@example.com", text: $emailAddress) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .keyboardType(.emailAddress) + .autocapitalization(.none) + .focused($focusedEmailField, equals: .address) + } + + // 主题 (必填) + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("主题") + .font(.subheadline) + .foregroundColor(.primary) + Text("*") + .foregroundColor(.red) + Spacer() + } + + TextField("邮件主题", text: $emailSubject) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .focused($focusedEmailField, equals: .subject) + } + + // 正文 (必填) + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("正文") + .font(.subheadline) + .foregroundColor(.primary) + Text("*") + .foregroundColor(.red) + Spacer() + } + + ZStack { + TextEditor(text: $emailBody) + .frame(minHeight: 120) + .padding(8) + .background(Color(.systemBackground)) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(focusedEmailField == .body ? Color.blue : Color(.systemGray4), lineWidth: 1) + ) + .focused($focusedEmailField, equals: .body) + .onChange(of: emailBody) { newValue in + // 限制最大字符数为1200 + if newValue.count > 1200 { + emailBody = String(newValue.prefix(1200)) + } + } + + // 占位符文本 + if emailBody.isEmpty && focusedEmailField != .body { + VStack { + HStack { + Text("输入邮件正文内容...") + .foregroundColor(.secondary) + .font(.body) + Spacer() + } + Spacer() + } + .padding(16) + .allowsHitTesting(false) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } + } + + // 字符计数 + HStack { + Spacer() + Text("\(emailBody.count)/1200") + .font(.caption) + .foregroundColor(emailBody.count >= 1200 ? .orange : .secondary) + } + } + + // CC地址 (可选) + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("抄送地址") + .font(.subheadline) + .foregroundColor(.primary) + Spacer() + } + + TextField("cc@example.com", text: $emailCc) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .keyboardType(.emailAddress) + .autocapitalization(.none) + .focused($focusedEmailField, equals: .cc) + } + + // BCC地址 (可选) + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("密送地址") + .font(.subheadline) + .foregroundColor(.primary) + Spacer() + } + + TextField("bcc@example.com", text: $emailBcc) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .keyboardType(.emailAddress) + .autocapitalization(.none) + .focused($focusedEmailField, equals: .bcc) + } + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("完成") { + focusedEmailField = nil + } + .foregroundColor(.blue) + .font(.system(size: 16, weight: .medium)) + } + } + } +} + +#Preview { + EmailInputView( + emailAddress: .constant(""), + emailSubject: .constant(""), + emailBody: .constant(""), + emailCc: .constant(""), + emailBcc: .constant("") + ) +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/FormView.swift b/MyQrCode/Views/Components/FormView.swift new file mode 100644 index 0000000..1d80db1 --- /dev/null +++ b/MyQrCode/Views/Components/FormView.swift @@ -0,0 +1,285 @@ +import SwiftUI + +// MARK: - 通用表单组件 +struct FormView: View { + let title: String? + let content: Content + let spacing: CGFloat + let padding: EdgeInsets + + init( + title: String? = nil, + spacing: CGFloat = 16, + padding: EdgeInsets = EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20), + @ViewBuilder content: () -> Content + ) { + self.title = title + self.spacing = spacing + self.padding = padding + self.content = content() + } + + var body: some View { + ScrollView { + VStack(spacing: spacing) { + if let title = title { + HStack { + Text(title) + .font(.title2) + .fontWeight(.semibold) + .foregroundColor(.primary) + + Spacer() + } + } + + content + } + .padding(padding) + } + .background(Color(.systemGroupedBackground)) + } +} + +// MARK: - 表单分组组件 +struct FormGroup: View { + let title: String? + let content: Content + let spacing: CGFloat + + init( + title: String? = nil, + spacing: CGFloat = 12, + @ViewBuilder content: () -> Content + ) { + self.title = title + self.spacing = spacing + self.content = content() + } + + var body: some View { + VStack(alignment: .leading, spacing: spacing) { + if let title = title { + HStack { + Text(title) + .font(.headline) + .foregroundColor(.primary) + + Spacer() + } + } + + content + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(color: .black.opacity(0.05), radius: 2, x: 0, y: 1) + } +} + +// MARK: - 表单行组件 +struct FormRow: View { + let title: String + let isRequired: Bool + let content: Content + let icon: String? + + init( + title: String, + isRequired: Bool = false, + icon: String? = nil, + @ViewBuilder content: () -> Content + ) { + self.title = title + self.isRequired = isRequired + self.icon = icon + self.content = content() + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + if let icon = icon { + Image(systemName: icon) + .font(.subheadline) + .foregroundColor(.blue) + } + + Text(title) + .font(.subheadline) + .foregroundColor(.primary) + + if isRequired { + Text("*") + .foregroundColor(.red) + .font(.subheadline) + } + + Spacer() + } + + content + } + } +} + +// MARK: - 表单操作按钮组件 +struct FormActionButton: View { + let title: String + let action: () -> Void + let isEnabled: Bool + let style: ButtonStyle + let icon: String? + + enum ButtonStyle { + case primary + case secondary + case destructive + case success + } + + init( + title: String, + action: @escaping () -> Void, + isEnabled: Bool = true, + style: ButtonStyle = .primary, + icon: String? = nil + ) { + self.title = title + self.action = action + self.isEnabled = isEnabled + self.style = style + self.icon = icon + } + + var body: some View { + Button(action: action) { + HStack(spacing: 8) { + if let icon = icon { + Image(systemName: icon) + .font(.system(size: 16, weight: .medium)) + } + + Text(title) + .font(.system(size: 16, weight: .semibold)) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(backgroundColor) + ) + .foregroundColor(foregroundColor) + } + .disabled(!isEnabled) + .opacity(isEnabled ? 1.0 : 0.6) + } + + private var backgroundColor: Color { + switch style { + case .primary: + return .blue + case .secondary: + return Color(.systemGray5) + case .destructive: + return .red + case .success: + return .green + } + } + + private var foregroundColor: Color { + switch style { + case .primary, .destructive, .success: + return .white + case .secondary: + return .primary + } + } +} + +// MARK: - 预定义的表单操作按钮 +extension FormActionButton { + static func primary( + title: String, + action: @escaping () -> Void, + isEnabled: Bool = true, + icon: String? = nil + ) -> FormActionButton { + FormActionButton( + title: title, + action: action, + isEnabled: isEnabled, + style: .primary, + icon: icon + ) + } + + static func secondary( + title: String, + action: @escaping () -> Void, + isEnabled: Bool = true, + icon: String? = nil + ) -> FormActionButton { + FormActionButton( + title: title, + action: action, + isEnabled: isEnabled, + style: .secondary, + icon: icon + ) + } + + static func destructive( + title: String, + action: @escaping () -> Void, + isEnabled: Bool = true, + icon: String? = nil + ) -> FormActionButton { + FormActionButton( + title: title, + action: action, + isEnabled: isEnabled, + style: .destructive, + icon: icon + ) + } + + static func success( + title: String, + action: @escaping () -> Void, + isEnabled: Bool = true, + icon: String? = nil + ) -> FormActionButton { + FormActionButton( + title: title, + action: action, + isEnabled: isEnabled, + style: .success, + icon: icon + ) + } +} + +#Preview { + FormView(title: "示例表单") { + FormGroup(title: "基本信息") { + FormRow(title: "用户名", isRequired: true, icon: "person") { + TextField("请输入用户名", text: .constant("")) + .textFieldStyle(RoundedBorderTextFieldStyle()) + } + + FormRow(title: "邮箱", isRequired: true, icon: "envelope") { + TextField("请输入邮箱", text: .constant("")) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .keyboardType(.emailAddress) + } + } + + FormGroup(title: "操作") { + FormActionButton.primary(title: "保存", action: {}) + FormActionButton.secondary(title: "取消", action: {}) + } + } +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/InputComponentFactory.swift b/MyQrCode/Views/Components/InputComponentFactory.swift new file mode 100644 index 0000000..eb69b42 --- /dev/null +++ b/MyQrCode/Views/Components/InputComponentFactory.swift @@ -0,0 +1,217 @@ +import SwiftUI + +// MARK: - 输入组件工厂 +struct InputComponentFactory { + + // 根据QR码类型返回相应的输入组件 + static func createInputComponent( + for qrCodeType: QRCodeType, + content: Binding, + emailAddress: Binding, + emailSubject: Binding, + emailBody: Binding, + emailCc: Binding, + emailBcc: Binding, + focusedEmailField: FocusState, + isContentFieldFocused: FocusState, + ssid: Binding, + password: Binding, + encryptionType: Binding, + focusedWiFiField: FocusState, + firstName: Binding, + lastName: Binding, + phone: Binding, + email: Binding, + company: Binding, + title: Binding, + address: Binding, + website: Binding, + focusedContactField: FocusState, + latitude: Binding, + longitude: Binding, + locationName: Binding, + focusedLocationField: FocusState, + eventTitle: Binding, + eventDescription: Binding, + startDate: Binding, + endDate: Binding, + location: Binding, + focusedCalendarField: FocusState, + username: Binding, + message: Binding, + focusedSocialField: FocusState, + phoneNumber: Binding, + phoneMessage: Binding, + focusedPhoneField: FocusState, + url: Binding, + isUrlFieldFocused: FocusState + ) -> AnyView { + + switch qrCodeType { + case .mail: + return AnyView( + EmailInputView( + emailAddress: emailAddress, + emailSubject: emailSubject, + emailBody: emailBody, + emailCc: emailCc, + emailBcc: emailBcc, + focusedEmailField: focusedEmailField + ) + ) + + case .wifi: + return AnyView( + WiFiInputView( + ssid: ssid, + password: password, + encryptionType: encryptionType, + focusedField: focusedWiFiField + ) + ) + + case .vcard, .mecard: + return AnyView( + ContactInputView( + firstName: firstName, + lastName: lastName, + phone: phone, + email: email, + company: company, + title: title, + address: address, + website: website, + focusedField: focusedContactField + ) + ) + + case .location: + return AnyView( + LocationInputView( + latitude: latitude, + longitude: longitude, + locationName: locationName, + focusedField: focusedLocationField + ) + ) + + case .calendar: + return AnyView( + CalendarInputView( + eventTitle: eventTitle, + eventDescription: eventDescription, + startDate: startDate, + endDate: endDate, + location: location + ) + ) + + case .instagram, .facebook, .twitter, .tiktok, .snapchat, .whatsapp, .viber, .spotify: + let platform = SocialInputView.SocialPlatform(rawValue: qrCodeType.rawValue.capitalized) ?? .instagram + return AnyView( + SocialInputView( + username: username, + message: message, + platform: platform, + focusedField: focusedSocialField + ) + ) + + case .phone, .sms: + let inputType: PhoneInputView.PhoneInputType = qrCodeType == .phone ? .phone : .sms + return AnyView( + PhoneInputView( + phoneNumber: phoneNumber, + message: phoneMessage, + inputType: inputType, + focusedField: focusedPhoneField + ) + ) + + case .url: + return AnyView( + URLInputView( + url: url, + isUrlFieldFocused: isUrlFieldFocused + ) + ) + + default: + // 默认使用通用文本输入组件 + return AnyView( + TextInputView( + content: content, + isContentFieldFocused: isContentFieldFocused, + placeholder: getPlaceholderText(for: qrCodeType), + maxCharacters: getMaxCharacters(for: qrCodeType) + ) + ) + } + } + + // 获取占位符文本 + private static func getPlaceholderText(for qrCodeType: QRCodeType) -> String { + switch qrCodeType { + case .text: + return "输入任意文本内容..." + case .phone: + return "输入电话号码..." + case .sms: + return "输入短信内容..." + case .wifi: + return "输入WiFi信息..." + case .vcard: + return "输入联系人信息..." + case .mecard: + return "输入联系人信息..." + case .location: + return "输入地理位置..." + case .calendar: + return "输入日历事件信息..." + case .instagram: + return "输入Instagram信息..." + case .facebook: + return "输入Facebook信息..." + case .spotify: + return "输入Spotify信息..." + case .twitter: + return "输入Twitter信息..." + case .whatsapp: + return "输入WhatsApp信息..." + case .viber: + return "输入Viber信息..." + case .snapchat: + return "输入Snapchat信息..." + case .tiktok: + return "输入TikTok信息..." + case .mail: + return "输入邮件内容..." + case .url: + return "输入网址..." + } + } + + // 获取最大字符数 + private static func getMaxCharacters(for qrCodeType: QRCodeType) -> Int { + switch qrCodeType { + case .text: + return 150 + case .phone, .sms: + return 100 + case .wifi: + return 200 + case .vcard, .mecard: + return 500 + case .location: + return 200 + case .calendar: + return 300 + case .instagram, .facebook, .twitter, .tiktok, .snapchat, .whatsapp, .viber, .spotify: + return 200 + case .mail: + return 1200 + case .url: + return 500 + } + } +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/InputFieldView.swift b/MyQrCode/Views/Components/InputFieldView.swift new file mode 100644 index 0000000..c4eb8f0 --- /dev/null +++ b/MyQrCode/Views/Components/InputFieldView.swift @@ -0,0 +1,208 @@ +import SwiftUI + +// MARK: - 通用输入字段组件 +struct InputFieldView: View { + let title: String + let isRequired: Bool + let placeholder: String + let text: Binding + let keyboardType: UIKeyboardType + let autocapitalization: UITextAutocapitalizationType + let icon: String? + let isSecure: Bool + let isFocused: Bool + let onFocusChange: (Bool) -> Void + + init( + title: String, + isRequired: Bool = false, + placeholder: String, + text: Binding, + keyboardType: UIKeyboardType = .default, + autocapitalization: UITextAutocapitalizationType = .sentences, + icon: String? = nil, + isSecure: Bool = false, + isFocused: Bool = false, + onFocusChange: @escaping (Bool) -> Void = { _ in } + ) { + self.title = title + self.isRequired = isRequired + self.placeholder = placeholder + self.text = text + self.keyboardType = keyboardType + self.autocapitalization = autocapitalization + self.icon = icon + self.isSecure = isSecure + self.isFocused = isFocused + self.onFocusChange = onFocusChange + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + InputTitleView.required(title, icon: icon) + + Group { + if isSecure { + SecureField(placeholder, text: text) + } else { + TextField(placeholder, text: text) + } + } + .textFieldStyle(RoundedBorderTextFieldStyle()) + .keyboardType(keyboardType) + .autocapitalization(autocapitalization) + .onTapGesture { + onFocusChange(true) + } + .onChange(of: isFocused) { newValue in + onFocusChange(newValue) + } + } + } +} + +// MARK: - 预定义的输入字段样式 +extension InputFieldView { + static func text( + title: String, + isRequired: Bool = false, + placeholder: String, + text: Binding, + icon: String? = nil, + isFocused: Bool = false, + onFocusChange: @escaping (Bool) -> Void = { _ in } + ) -> InputFieldView { + InputFieldView( + title: title, + isRequired: isRequired, + placeholder: placeholder, + text: text, + icon: icon, + isFocused: isFocused, + onFocusChange: onFocusChange + ) + } + + static func email( + title: String, + isRequired: Bool = false, + placeholder: String, + text: Binding, + icon: String? = nil, + isFocused: Bool = false, + onFocusChange: @escaping (Bool) -> Void = { _ in } + ) -> InputFieldView { + InputFieldView( + title: title, + isRequired: isRequired, + placeholder: placeholder, + text: text, + keyboardType: .emailAddress, + autocapitalization: .sentences, + icon: icon, + isFocused: isFocused, + onFocusChange: onFocusChange + ) + } + + static func phone( + title: String, + isRequired: Bool = false, + placeholder: String, + text: Binding, + icon: String? = nil, + isFocused: Bool = false, + onFocusChange: @escaping (Bool) -> Void = { _ in } + ) -> InputFieldView { + InputFieldView( + title: title, + isRequired: isRequired, + placeholder: placeholder, + text: text, + keyboardType: .phonePad, + icon: icon, + isFocused: isFocused, + onFocusChange: onFocusChange + ) + } + + static func url( + title: String, + isRequired: Bool = false, + placeholder: String, + text: Binding, + icon: String? = nil, + isFocused: Bool = false, + onFocusChange: @escaping (Bool) -> Void = { _ in } + ) -> InputFieldView { + InputFieldView( + title: title, + isRequired: isRequired, + placeholder: placeholder, + text: text, + keyboardType: .URL, + autocapitalization: .sentences, + icon: icon, + isFocused: isFocused, + onFocusChange: onFocusChange + ) + } + + static func password( + title: String, + isRequired: Bool = false, + placeholder: String, + text: Binding, + icon: String? = nil, + isFocused: Bool = false, + onFocusChange: @escaping (Bool) -> Void = { _ in } + ) -> InputFieldView { + InputFieldView( + title: title, + isRequired: isRequired, + placeholder: placeholder, + text: text, + icon: icon, + isSecure: true, + isFocused: isFocused, + onFocusChange: onFocusChange + ) + } +} + +#Preview { + VStack(spacing: 16) { + InputFieldView.text( + title: "用户名", + isRequired: true, + placeholder: "请输入用户名", + text: .constant(""), + icon: "person" + ) + + InputFieldView.email( + title: "邮箱地址", + isRequired: true, + placeholder: "user@example.com", + text: .constant(""), + icon: "envelope" + ) + + InputFieldView.phone( + title: "电话号码", + isRequired: true, + placeholder: "+86 138 0013 8000", + text: .constant(""), + icon: "phone" + ) + + InputFieldView.password( + title: "密码", + isRequired: true, + placeholder: "请输入密码", + text: .constant(""), + icon: "lock" + ) + } + .padding() +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/InputHintView.swift b/MyQrCode/Views/Components/InputHintView.swift new file mode 100644 index 0000000..f82f7ae --- /dev/null +++ b/MyQrCode/Views/Components/InputHintView.swift @@ -0,0 +1,70 @@ +import SwiftUI + +// MARK: - 输入提示组件 +struct InputHintView: View { + let hint: String + let icon: String + let color: Color + + init(hint: String, icon: String = "info.circle", color: Color = .blue) { + self.hint = hint + self.icon = icon + self.color = color + } + + var body: some View { + VStack(spacing: 12) { + HStack { + Image(systemName: icon) + .font(.caption) + .foregroundColor(color) + + Text("输入提示") + .font(.caption) + .foregroundColor(.primary) + + Spacer() + } + + Text(hint) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(nil) + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(color.opacity(0.1)) + ) + } +} + +// MARK: - 预定义的提示样式 +extension InputHintView { + static func info(hint: String) -> InputHintView { + InputHintView(hint: hint, icon: "info.circle", color: .blue) + } + + static func warning(hint: String) -> InputHintView { + InputHintView(hint: hint, icon: "exclamationmark.triangle", color: .orange) + } + + static func success(hint: String) -> InputHintView { + InputHintView(hint: hint, icon: "checkmark.circle", color: .green) + } + + static func error(hint: String) -> InputHintView { + InputHintView(hint: hint, icon: "xmark.circle", color: .red) + } +} + +#Preview { + VStack(spacing: 16) { + InputHintView.info(hint: "这是一个信息提示") + InputHintView.warning(hint: "这是一个警告提示") + InputHintView.success(hint: "这是一个成功提示") + InputHintView.error(hint: "这是一个错误提示") + } + .padding() +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/InputTitleView.swift b/MyQrCode/Views/Components/InputTitleView.swift new file mode 100644 index 0000000..f64cf47 --- /dev/null +++ b/MyQrCode/Views/Components/InputTitleView.swift @@ -0,0 +1,58 @@ +import SwiftUI + +// MARK: - 输入标题组件 +struct InputTitleView: View { + let title: String + let isRequired: Bool + let icon: String? + + init(_ title: String, isRequired: Bool = false, icon: String? = nil) { + self.title = title + self.isRequired = isRequired + self.icon = icon + } + + var body: some View { + HStack(spacing: 8) { + if let icon = icon { + Image(systemName: icon) + .font(.subheadline) + .foregroundColor(.blue) + } + + Text(title) + .font(.subheadline) + .foregroundColor(.primary) + + if isRequired { + Text("*") + .foregroundColor(.red) + .font(.subheadline) + } + + Spacer() + } + } +} + +// MARK: - 预定义的标题样式 +extension InputTitleView { + static func required(_ title: String, icon: String? = nil) -> InputTitleView { + InputTitleView(title, isRequired: true, icon: icon) + } + + static func optional(_ title: String, icon: String? = nil) -> InputTitleView { + InputTitleView(title, isRequired: false, icon: icon) + } +} + +#Preview { + VStack(spacing: 16) { + InputTitleView.required("必填字段", icon: "star.fill") + InputTitleView.optional("可选字段", icon: "circle") + InputTitleView("普通标题") + InputTitleView.required("邮箱地址", icon: "envelope") + InputTitleView.required("密码", icon: "lock") + } + .padding() +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/KeyboardToolbarView.swift b/MyQrCode/Views/Components/KeyboardToolbarView.swift new file mode 100644 index 0000000..548586c --- /dev/null +++ b/MyQrCode/Views/Components/KeyboardToolbarView.swift @@ -0,0 +1,273 @@ +import SwiftUI + +// MARK: - 通用键盘工具栏组件 +struct KeyboardToolbarView: View { + let onDone: () -> Void + let showDoneButton: Bool + let additionalButtons: [KeyboardToolbarButton] + + init( + onDone: @escaping () -> Void = {}, + showDoneButton: Bool = true, + additionalButtons: [KeyboardToolbarButton] = [] + ) { + self.onDone = onDone + self.showDoneButton = showDoneButton + self.additionalButtons = additionalButtons + } + + var body: some View { + Group { + // 左侧按钮 + HStack(spacing: 12) { + ForEach(additionalButtons.filter { $0.position == .left }, id: \.id) { button in + Button(action: button.action) { + HStack(spacing: 4) { + if let icon = button.icon { + Image(systemName: icon) + .font(.system(size: 16, weight: .medium)) + } + Text(button.title) + .font(.system(size: 16, weight: .medium)) + } + .foregroundColor(button.color) + } + } + } + + Spacer() + + // 右侧按钮 + HStack(spacing: 12) { + ForEach(additionalButtons.filter { $0.position == .right }, id: \.id) { button in + Button(action: button.action) { + HStack(spacing: 4) { + if let icon = button.icon { + Image(systemName: icon) + .font(.system(size: 16, weight: .medium)) + } + Text(button.title) + .font(.system(size: 16, weight: .medium)) + } + .foregroundColor(button.color) + } + } + + if showDoneButton { + Button("完成", action: onDone) + .foregroundColor(.blue) + .font(.system(size: 16, weight: .medium)) + } + } + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + // 左侧按钮 + HStack(spacing: 12) { + ForEach(additionalButtons.filter { $0.position == .left }, id: \.id) { button in + Button(action: button.action) { + HStack(spacing: 4) { + if let icon = button.icon { + Image(systemName: icon) + .font(.system(size: 16, weight: .medium)) + } + Text(button.title) + .font(.system(size: 16, weight: .medium)) + } + .foregroundColor(button.color) + } + } + } + + Spacer() + + // 右侧按钮 + HStack(spacing: 12) { + ForEach(additionalButtons.filter { $0.position == .right }, id: \.id) { button in + Button(action: button.action) { + HStack(spacing: 4) { + if let icon = button.icon { + Image(systemName: icon) + .font(.system(size: 16, weight: .medium)) + } + Text(button.title) + .font(.system(size: 16, weight: .medium)) + } + .foregroundColor(button.color) + } + } + + if showDoneButton { + Button("完成", action: onDone) + .foregroundColor(.blue) + .font(.system(size: 16, weight: .medium)) + } + } + } + } + } +} + +// MARK: - 键盘工具栏按钮 +struct KeyboardToolbarButton { + let id = UUID() + let title: String + let icon: String? + let color: Color + let position: ButtonPosition + let action: () -> Void + + enum ButtonPosition { + case left, right + } + + init( + title: String, + icon: String? = nil, + color: Color = .blue, + position: ButtonPosition = .right, + action: @escaping () -> Void + ) { + self.title = title + self.icon = icon + self.color = color + self.position = position + self.action = action + } +} + +// MARK: - 预定义的工具栏按钮 +extension KeyboardToolbarButton { + static func clear( + text: Binding, + position: ButtonPosition = .left + ) -> KeyboardToolbarButton { + KeyboardToolbarButton( + title: "清空", + icon: "trash", + color: .red, + position: position + ) { + text.wrappedValue = "" + } + } + + static func copy( + text: String, + position: ButtonPosition = .left + ) -> KeyboardToolbarButton { + KeyboardToolbarButton( + title: "复制", + icon: "doc.on.doc", + color: .blue, + position: position + ) { + UIPasteboard.general.string = text + } + } + + static func paste( + text: Binding, + position: ButtonPosition = .left + ) -> KeyboardToolbarButton { + KeyboardToolbarButton( + title: "粘贴", + icon: "doc.on.clipboard", + color: .green, + position: position + ) { + if let pastedText = UIPasteboard.general.string { + text.wrappedValue = pastedText + } + } + } + + static func next( + action: @escaping () -> Void, + position: ButtonPosition = .right + ) -> KeyboardToolbarButton { + KeyboardToolbarButton( + title: "下一个", + icon: "arrow.right", + color: .blue, + position: position, + action: action + ) + } + + static func previous( + action: @escaping () -> Void, + position: ButtonPosition = .left + ) -> KeyboardToolbarButton { + KeyboardToolbarButton( + title: "上一个", + icon: "arrow.left", + color: .blue, + position: position, + action: action + ) + } +} + +// MARK: - 预定义的工具栏样式 +extension KeyboardToolbarView { + static func simple(onDone: @escaping () -> Void = {}) -> KeyboardToolbarView { + KeyboardToolbarView(onDone: onDone) + } + + static func withClear( + text: Binding, + onDone: @escaping () -> Void = {} + ) -> KeyboardToolbarView { + KeyboardToolbarView( + onDone: onDone, + additionalButtons: [ + .clear(text: text) + ] + ) + } + + static func withCopyPaste( + text: Binding, + onDone: @escaping () -> Void = {} + ) -> KeyboardToolbarView { + KeyboardToolbarView( + onDone: onDone, + additionalButtons: [ + .copy(text: text.wrappedValue), + .paste(text: text) + ] + ) + } + + static func withNavigation( + onPrevious: @escaping () -> Void, + onNext: @escaping () -> Void, + onDone: @escaping () -> Void = {} + ) -> KeyboardToolbarView { + KeyboardToolbarView( + onDone: onDone, + additionalButtons: [ + .previous(action: onPrevious), + .next(action: onNext) + ] + ) + } +} + +#Preview { + VStack(spacing: 16) { + Text("简单工具栏") + .font(.headline) + + Text("带清空按钮的工具栏") + .font(.headline) + + Text("带复制粘贴的工具栏") + .font(.headline) + + Text("带导航的工具栏") + .font(.headline) + } + .padding() +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/ListView.swift b/MyQrCode/Views/Components/ListView.swift new file mode 100644 index 0000000..6b21378 --- /dev/null +++ b/MyQrCode/Views/Components/ListView.swift @@ -0,0 +1,318 @@ +import SwiftUI + +// MARK: - 通用列表组件 +struct ListView: View where Data.Element: Identifiable { + let data: Data + let content: (Data.Element) -> Content + let spacing: CGFloat + let padding: EdgeInsets + let backgroundColor: Color + + init( + data: Data, + spacing: CGFloat = 12, + padding: EdgeInsets = EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0), + backgroundColor: Color = Color(.systemGroupedBackground), + @ViewBuilder content: @escaping (Data.Element) -> Content + ) { + self.data = data + self.spacing = spacing + self.padding = padding + self.backgroundColor = backgroundColor + self.content = content + } + + var body: some View { + ScrollView { + LazyVStack(spacing: spacing) { + ForEach(data) { item in + content(item) + } + } + .padding(padding) + } + .background(backgroundColor) + } +} + +// MARK: - 列表项组件 +struct ListItem: View { + let content: Content + let padding: EdgeInsets + let cornerRadius: CGFloat + let backgroundColor: Color + let shadowColor: Color + let shadowRadius: CGFloat + let shadowOffset: CGSize + let onTap: (() -> Void)? + + init( + padding: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16), + cornerRadius: CGFloat = 12, + backgroundColor: Color = Color(.systemBackground), + shadowColor: Color = .black.opacity(0.05), + shadowRadius: CGFloat = 4, + shadowOffset: CGSize = CGSize(width: 0, height: 2), + onTap: (() -> Void)? = nil, + @ViewBuilder content: () -> Content + ) { + self.padding = padding + self.cornerRadius = cornerRadius + self.backgroundColor = backgroundColor + self.shadowColor = shadowColor + self.shadowRadius = shadowRadius + self.shadowOffset = shadowOffset + self.onTap = onTap + self.content = content() + } + + var body: some View { + Group { + if let onTap = onTap { + Button(action: onTap) { + content + .padding(padding) + .background(backgroundColor) + .cornerRadius(cornerRadius) + .shadow( + color: shadowColor, + radius: shadowRadius, + x: shadowOffset.width, + y: shadowOffset.height + ) + } + .buttonStyle(PlainButtonStyle()) + } else { + content + .padding(padding) + .background(backgroundColor) + .cornerRadius(cornerRadius) + .shadow( + color: shadowColor, + radius: shadowRadius, + x: shadowOffset.width, + y: shadowOffset.height + ) + } + } + } +} + +// MARK: - 预定义的列表项样式 +extension ListItem { + static func standard( + onTap: (() -> Void)? = nil, + @ViewBuilder content: () -> U + ) -> ListItem { + ListItem(onTap: onTap, content: content) + } + + static func compact( + onTap: (() -> Void)? = nil, + @ViewBuilder content: () -> U + ) -> ListItem { + ListItem( + padding: EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12), + cornerRadius: 8, + onTap: onTap, + content: content + ) + } + + static func elevated( + onTap: (() -> Void)? = nil, + @ViewBuilder content: () -> U + ) -> ListItem { + ListItem( + shadowColor: .black.opacity(0.1), + shadowRadius: 8, + shadowOffset: CGSize(width: 0, height: 4), + onTap: onTap, + content: content + ) + } +} + +// MARK: - 空状态组件 +struct EmptyStateView: View { + let icon: String + let title: String + let subtitle: String? + let actionTitle: String? + let action: (() -> Void)? + + init( + icon: String, + title: String, + subtitle: String? = nil, + actionTitle: String? = nil, + action: (() -> Void)? = nil + ) { + self.icon = icon + self.title = title + self.subtitle = subtitle + self.actionTitle = actionTitle + self.action = action + } + + var body: some View { + VStack(spacing: 20) { + Image(systemName: icon) + .font(.system(size: 60)) + .foregroundColor(.secondary) + + VStack(spacing: 8) { + Text(title) + .font(.title2) + .fontWeight(.semibold) + .foregroundColor(.primary) + + if let subtitle = subtitle { + Text(subtitle) + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + } + + if let actionTitle = actionTitle, let action = action { + Button(action: action) { + Text(actionTitle) + .font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.blue) + .padding(.horizontal, 20) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color.blue.opacity(0.1)) + ) + } + } + } + .padding(40) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +// MARK: - 加载状态组件 +struct LoadingStateView: View { + let message: String + + init(message: String = "加载中...") { + self.message = message + } + + var body: some View { + VStack(spacing: 16) { + ProgressView() + .scaleEffect(1.2) + + Text(message) + .font(.subheadline) + .foregroundColor(.secondary) + } + .padding(40) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +// MARK: - 错误状态组件 +struct ErrorStateView: View { + let title: String + let message: String + let retryAction: (() -> Void)? + + init( + title: String = "出错了", + message: String = "加载失败,请重试", + retryAction: (() -> Void)? = nil + ) { + self.title = title + self.message = message + self.retryAction = retryAction + } + + var body: some View { + VStack(spacing: 20) { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 60)) + .foregroundColor(.orange) + + VStack(spacing: 8) { + Text(title) + .font(.title2) + .fontWeight(.semibold) + .foregroundColor(.primary) + + Text(message) + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + + if let retryAction = retryAction { + Button(action: retryAction) { + Text("重试") + .font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.blue) + .padding(.horizontal, 20) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color.blue.opacity(0.1)) + ) + } + } + } + .padding(40) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +#Preview { + VStack(spacing: 20) { + // 列表视图 + ListView(data: Array(1...5).map { ListItemData(id: $0, title: "项目 \($0)") }) { item in + ListItem.standard { + AnyView( + HStack { + Text(item.title) + .font(.body) + Spacer() + Image(systemName: "chevron.right") + .font(.caption) + .foregroundColor(.secondary) + } + ) + } + } + .frame(height: 300) + + // 空状态 + EmptyStateView( + icon: "tray", + title: "暂无数据", + subtitle: "这里还没有任何内容", + actionTitle: "添加内容", + action: {} + ) + + // 加载状态 + LoadingStateView(message: "正在加载数据...") + + // 错误状态 + ErrorStateView( + title: "网络错误", + message: "无法连接到服务器,请检查网络连接", + retryAction: {} + ) + } +} + +// MARK: - 示例数据 +struct ListItemData: Identifiable { + let id: Int + let title: String +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/LocationInputView.swift b/MyQrCode/Views/Components/LocationInputView.swift new file mode 100644 index 0000000..32f5411 --- /dev/null +++ b/MyQrCode/Views/Components/LocationInputView.swift @@ -0,0 +1,111 @@ +import SwiftUI + +// MARK: - 地理位置输入组件 +struct LocationInputView: View { + @Binding var latitude: String + @Binding var longitude: String + @Binding var locationName: String + @FocusState var focusedField: LocationField? + + // 地理位置字段枚举 + enum LocationField: Hashable { + case latitude, longitude, locationName + } + + var body: some View { + VStack(spacing: 16) { + // 位置名称 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("位置名称") + .font(.subheadline) + .foregroundColor(.primary) + Spacer() + } + + TextField("例如:纽约时代广场", text: $locationName) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .focused($focusedField, equals: .locationName) + } + + // 坐标输入 + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("纬度") + .font(.subheadline) + .foregroundColor(.primary) + Text("*") + .foregroundColor(.red) + Spacer() + } + + TextField("40.7589", text: $latitude) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .keyboardType(.decimalPad) + .focused($focusedField, equals: .latitude) + } + + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("经度") + .font(.subheadline) + .foregroundColor(.primary) + Text("*") + .foregroundColor(.red) + Spacer() + } + + TextField("-73.9851", text: $longitude) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .keyboardType(.decimalPad) + .focused($focusedField, equals: .longitude) + } + } + + // 坐标格式说明 + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: "info.circle") + .font(.caption) + .foregroundColor(.blue) + + Text("坐标格式说明") + .font(.caption) + .foregroundColor(.primary) + + Spacer() + } + + Text("• 纬度范围:-90 到 90\n• 经度范围:-180 到 180\n• 使用小数点分隔,如:40.7589") + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(nil) + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color.blue.opacity(0.1)) + ) + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("完成") { + focusedField = nil + } + .foregroundColor(.blue) + .font(.system(size: 16, weight: .medium)) + } + } + } +} + +#Preview { + LocationInputView( + latitude: .constant(""), + longitude: .constant(""), + locationName: .constant("") + ) +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/PhoneInputView.swift b/MyQrCode/Views/Components/PhoneInputView.swift new file mode 100644 index 0000000..ddff0c9 --- /dev/null +++ b/MyQrCode/Views/Components/PhoneInputView.swift @@ -0,0 +1,163 @@ +import SwiftUI + +// MARK: - 电话输入组件 +struct PhoneInputView: View { + @Binding var phoneNumber: String + @Binding var message: String + let inputType: PhoneInputType + @FocusState var focusedField: PhoneField? + + // 电话输入类型枚举 + enum PhoneInputType: String, CaseIterable { + case phone = "Phone" + case sms = "SMS" + + var displayName: String { + switch self { + case .phone: return "电话" + case .sms: return "短信" + } + } + + var icon: String { + switch self { + case .phone: return "phone" + case .sms: return "message" + } + } + + var placeholder: String { + switch self { + case .phone: return "+86 138 0013 8000" + case .sms: return "输入短信内容" + } + } + + var hint: String { + switch self { + case .phone: return "输入电话号码,支持国际格式" + case .sms: return "输入短信内容,将生成可发送的链接" + } + } + } + + // 电话字段枚举 + enum PhoneField: Hashable { + case phoneNumber, message + } + + var body: some View { + VStack(spacing: 16) { + // 类型信息 + HStack { + Image(systemName: inputType.icon) + .font(.title2) + .foregroundColor(.blue) + + VStack(alignment: .leading, spacing: 2) { + Text(inputType.displayName) + .font(.headline) + .foregroundColor(.primary) + + Text(inputType.hint) + .font(.caption) + .foregroundColor(.secondary) + } + + Spacer() + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color.blue.opacity(0.1)) + ) + + // 电话号码 (必填) + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("电话号码") + .font(.subheadline) + .foregroundColor(.primary) + Text("*") + .foregroundColor(.red) + Spacer() + } + + TextField(inputType.placeholder, text: $phoneNumber) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .keyboardType(.phonePad) + .focused($focusedField, equals: .phoneNumber) + } + + // 短信内容 (仅SMS类型) + if inputType == .sms { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("短信内容") + .font(.subheadline) + .foregroundColor(.primary) + Spacer() + } + + TextField("输入短信内容", text: $message) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .focused($focusedField, equals: .message) + } + } + + // 格式说明 + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: "info.circle") + .font(.caption) + .foregroundColor(.blue) + + Text("格式说明") + .font(.caption) + .foregroundColor(.primary) + + Spacer() + } + + Text(getFormatHint()) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(nil) + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color.blue.opacity(0.1)) + ) + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("完成") { + focusedField = nil + } + .foregroundColor(.blue) + .font(.system(size: 16, weight: .medium)) + } + } + } + + private func getFormatHint() -> String { + switch inputType { + case .phone: + return "• 支持国际格式:+86 138 0013 8000\n• 或本地格式:138 0013 8000\n• 将生成 tel: 链接" + case .sms: + return "• 输入电话号码和短信内容\n• 将生成 sms: 链接\n• 用户点击可直接发送短信" + } + } +} + +#Preview { + PhoneInputView( + phoneNumber: .constant(""), + message: .constant(""), + inputType: .phone + ) +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/PickerView.swift b/MyQrCode/Views/Components/PickerView.swift new file mode 100644 index 0000000..5f0c751 --- /dev/null +++ b/MyQrCode/Views/Components/PickerView.swift @@ -0,0 +1,108 @@ +import SwiftUI + +// MARK: - 简化的选择器组件 +struct SimplePickerView: View { + let title: String + let isRequired: Bool + let selection: Binding + let options: [T] + let optionTitle: (T) -> String + let icon: String? + + init( + title: String, + isRequired: Bool = false, + selection: Binding, + options: [T], + optionTitle: @escaping (T) -> String, + icon: String? = nil + ) { + self.title = title + self.isRequired = isRequired + self.selection = selection + self.options = options + self.optionTitle = optionTitle + self.icon = icon + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + InputTitleView.required(title, icon: icon) + + Picker(title, selection: selection) { + ForEach(options, id: \.self) { option in + Text(optionTitle(option)).tag(option) + } + } + .pickerStyle(.menu) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color(.systemGray6)) + ) + } + } +} + +// MARK: - 常用的选择器类型 +extension SimplePickerView { + static func wifiEncryption( + selection: Binding, + icon: String? = "lock" + ) -> SimplePickerView { + SimplePickerView( + title: "加密类型", + selection: selection, + options: WiFiInputView.WiFiEncryptionType.allCases, + optionTitle: { $0.displayName }, + icon: icon + ) + } + + static func socialPlatform( + selection: Binding, + icon: String? = "globe" + ) -> SimplePickerView { + SimplePickerView( + title: "社交平台", + selection: selection, + options: SocialInputView.SocialPlatform.allCases, + optionTitle: { $0.displayName }, + icon: icon + ) + } + + static func phoneType( + selection: Binding, + icon: String? = "phone" + ) -> SimplePickerView { + SimplePickerView( + title: "电话类型", + selection: selection, + options: PhoneInputView.PhoneInputType.allCases, + optionTitle: { $0.displayName }, + icon: icon + ) + } +} + +#Preview { + VStack(spacing: 16) { + // WiFi加密类型选择器 + SimplePickerView.wifiEncryption( + selection: .constant(.wpa2) + ) + + // 社交平台选择器 + SimplePickerView.socialPlatform( + selection: .constant(.instagram) + ) + + // 电话类型选择器 + SimplePickerView.phoneType( + selection: .constant(.phone) + ) + } + .padding() +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/QRCodePreviewView.swift b/MyQrCode/Views/Components/QRCodePreviewView.swift new file mode 100644 index 0000000..4cb276c --- /dev/null +++ b/MyQrCode/Views/Components/QRCodePreviewView.swift @@ -0,0 +1,90 @@ +import SwiftUI + +// MARK: - QR码预览组件 +struct QRCodePreviewView: View { + let qrCodeImage: UIImage? + let formattedContent: String + let qrCodeType: QRCodeType + + var body: some View { + VStack(spacing: 16) { + HStack { + Text("预览") + .font(.headline) + .foregroundColor(.primary) + + Spacer() + } + + // 二维码预览卡片 + VStack(spacing: 16) { + // 二维码图片 + if let qrImage = qrCodeImage { + Image(uiImage: qrImage) + .interpolation(.none) + .resizable() + .scaledToFit() + .frame(width: 200, height: 200) + .background(Color.white) + .cornerRadius(12) + .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4) + } else { + // 占位符 + RoundedRectangle(cornerRadius: 12) + .fill(Color(.systemGray5)) + .frame(width: 200, height: 200) + .overlay( + VStack(spacing: 8) { + Image(systemName: "qrcode") + .font(.system(size: 40)) + .foregroundColor(.secondary) + Text("无法生成二维码") + .font(.caption) + .foregroundColor(.secondary) + } + ) + } + + // 内容预览卡片 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("内容") + .font(.caption) + .foregroundColor(.secondary) + + Spacer() + + Text(qrCodeType.displayName) + .font(.caption) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.orange.opacity(0.1)) + .foregroundColor(.orange) + .cornerRadius(4) + } + + Text(formattedContent) + .font(.body) + .foregroundColor(.primary) + .textSelection(.enabled) + .lineLimit(nil) + } + .padding() + .background(Color(.systemGray6)) + .cornerRadius(8) + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 2) + } + } +} + +#Preview { + QRCodePreviewView( + qrCodeImage: nil, + formattedContent: "示例内容", + qrCodeType: .text + ) +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/SocialInputView.swift b/MyQrCode/Views/Components/SocialInputView.swift new file mode 100644 index 0000000..f6395d8 --- /dev/null +++ b/MyQrCode/Views/Components/SocialInputView.swift @@ -0,0 +1,199 @@ +import SwiftUI + +// MARK: - 社交平台输入组件 +struct SocialInputView: View { + @Binding var username: String + @Binding var message: String + let platform: SocialPlatform + @FocusState var focusedField: SocialField? + + // 社交平台枚举 + enum SocialPlatform: String, CaseIterable { + case instagram = "Instagram" + case facebook = "Facebook" + case twitter = "Twitter" + case tiktok = "TikTok" + case snapchat = "Snapchat" + case whatsapp = "WhatsApp" + case viber = "Viber" + case spotify = "Spotify" + + var displayName: String { + switch self { + case .instagram: return "Instagram" + case .facebook: return "Facebook" + case .twitter: return "Twitter" + case .tiktok: return "TikTok" + case .snapchat: return "Snapchat" + case .whatsapp: return "WhatsApp" + case .viber: return "Viber" + case .spotify: return "Spotify" + } + } + + var icon: String { + switch self { + case .instagram: return "camera" + case .facebook: return "person.2" + case .twitter: return "bird" + case .tiktok: return "music.note" + case .snapchat: return "ghost" + case .whatsapp: return "message" + case .viber: return "phone" + case .spotify: return "music.note.list" + } + } + + var placeholder: String { + switch self { + case .instagram: return "用户名或链接" + case .facebook: return "用户名或链接" + case .twitter: return "用户名或链接" + case .tiktok: return "用户名或链接" + case .snapchat: return "用户名" + case .whatsapp: return "消息内容" + case .viber: return "消息内容" + case .spotify: return "歌曲或播放列表链接" + } + } + + var hint: String { + switch self { + case .instagram: return "输入Instagram用户名或完整链接" + case .facebook: return "输入Facebook用户名或完整链接" + case .twitter: return "输入Twitter用户名或完整链接" + case .tiktok: return "输入TikTok用户名或完整链接" + case .snapchat: return "输入Snapchat用户名" + case .whatsapp: return "输入WhatsApp消息内容" + case .viber: return "输入Viber消息内容" + case .spotify: return "输入Spotify歌曲或播放列表链接" + } + } + } + + // 社交字段枚举 + enum SocialField: Hashable { + case username, message + } + + var body: some View { + VStack(spacing: 16) { + // 平台信息 + HStack { + Image(systemName: platform.icon) + .font(.title2) + .foregroundColor(.blue) + + VStack(alignment: .leading, spacing: 2) { + Text(platform.displayName) + .font(.headline) + .foregroundColor(.primary) + + Text(platform.hint) + .font(.caption) + .foregroundColor(.secondary) + } + + Spacer() + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color.blue.opacity(0.1)) + ) + + // 用户名/链接 (必填) + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(platform == .whatsapp || platform == .viber ? "消息内容" : "用户名/链接") + .font(.subheadline) + .foregroundColor(.primary) + Text("*") + .foregroundColor(.red) + Spacer() + } + + TextField(platform.placeholder, text: $username) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocapitalization(.none) + .focused($focusedField, equals: .username) + } + + // 消息内容 (仅WhatsApp和Viber) + if platform == .whatsapp || platform == .viber { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("消息内容") + .font(.subheadline) + .foregroundColor(.primary) + Spacer() + } + + TextField("输入消息内容", text: $message) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .focused($focusedField, equals: .message) + } + } + + // 格式说明 + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: "info.circle") + .font(.caption) + .foregroundColor(.blue) + + Text("格式说明") + .font(.caption) + .foregroundColor(.primary) + + Spacer() + } + + Text(getFormatHint()) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(nil) + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color.blue.opacity(0.1)) + ) + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("完成") { + focusedField = nil + } + .foregroundColor(.blue) + .font(.system(size: 16, weight: .medium)) + } + } + } + + private func getFormatHint() -> String { + switch platform { + case .instagram, .facebook, .twitter, .tiktok: + return "• 可以输入用户名(如:username)\n• 或输入完整链接(如:https://instagram.com/username)" + case .snapchat: + return "• 输入Snapchat用户名\n• 例如:username" + case .whatsapp: + return "• 输入WhatsApp消息内容\n• 将生成可分享的链接" + case .viber: + return "• 输入Viber消息内容\n• 将生成可分享的链接" + case .spotify: + return "• 输入歌曲或播放列表链接\n• 或输入Spotify ID" + } + } +} + +#Preview { + SocialInputView( + username: .constant(""), + message: .constant(""), + platform: .instagram + ) +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/TextEditorView.swift b/MyQrCode/Views/Components/TextEditorView.swift new file mode 100644 index 0000000..7788aea --- /dev/null +++ b/MyQrCode/Views/Components/TextEditorView.swift @@ -0,0 +1,200 @@ +import SwiftUI + +// MARK: - 通用文本编辑器组件 +struct TextEditorView: View { + let title: String + let isRequired: Bool + let placeholder: String + let text: Binding + let maxCharacters: Int + let minHeight: CGFloat + let icon: String? + let isFocused: Bool + let onFocusChange: (Bool) -> Void + + init( + title: String, + isRequired: Bool = false, + placeholder: String, + text: Binding, + maxCharacters: Int = 1000, + minHeight: CGFloat = 120, + icon: String? = nil, + isFocused: Bool = false, + onFocusChange: @escaping (Bool) -> Void = { _ in } + ) { + self.title = title + self.isRequired = isRequired + self.placeholder = placeholder + self.text = text + self.maxCharacters = maxCharacters + self.minHeight = minHeight + self.icon = icon + self.isFocused = isFocused + self.onFocusChange = onFocusChange + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + InputTitleView.required(title, icon: icon) + + ZStack { + // 文本编辑器主体 + TextEditor(text: text) + .frame(minHeight: minHeight) + .padding(8) + .background(Color(.systemBackground)) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(isFocused ? Color.blue : Color(.systemGray4), lineWidth: 1) + ) + .onChange(of: text.wrappedValue) { newValue in + // 限制最大字符数 + if newValue.count > maxCharacters { + text.wrappedValue = String(newValue.prefix(maxCharacters)) + } + } + .onTapGesture { + onFocusChange(true) + } + + // 占位符文本 - 左上角对齐 + if text.wrappedValue.isEmpty && !isFocused { + VStack { + HStack { + Text(placeholder) + .foregroundColor(.secondary) + .font(.body) + Spacer() + } + Spacer() + } + .padding(16) + .allowsHitTesting(false) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } + } + + // 字符计数 + HStack { + Spacer() + Text("\(text.wrappedValue.count)/\(maxCharacters)") + .font(.caption) + .foregroundColor(getCharacterCountColor()) + } + } + } + + private func getCharacterCountColor() -> Color { + let count = text.wrappedValue.count + if count >= maxCharacters { + return .orange + } else if count >= Int(Double(maxCharacters) * 0.9) { + return .blue + } else { + return .secondary + } + } +} + +// MARK: - 预定义的文本编辑器样式 +extension TextEditorView { + static func description( + title: String, + isRequired: Bool = false, + placeholder: String, + text: Binding, + maxCharacters: Int = 500, + icon: String? = nil, + isFocused: Bool = false, + onFocusChange: @escaping (Bool) -> Void = { _ in } + ) -> TextEditorView { + TextEditorView( + title: title, + isRequired: isRequired, + placeholder: placeholder, + text: text, + maxCharacters: maxCharacters, + minHeight: 100, + icon: icon, + isFocused: isFocused, + onFocusChange: onFocusChange + ) + } + + static func longText( + title: String, + isRequired: Bool = false, + placeholder: String, + text: Binding, + maxCharacters: Int = 1000, + icon: String? = nil, + isFocused: Bool = false, + onFocusChange: @escaping (Bool) -> Void = { _ in } + ) -> TextEditorView { + TextEditorView( + title: title, + isRequired: isRequired, + placeholder: placeholder, + text: text, + maxCharacters: maxCharacters, + minHeight: 150, + icon: icon, + isFocused: isFocused, + onFocusChange: onFocusChange + ) + } + + static func emailBody( + title: String, + isRequired: Bool = false, + placeholder: String, + text: Binding, + maxCharacters: Int = 1200, + icon: String? = nil, + isFocused: Bool = false, + onFocusChange: @escaping (Bool) -> Void = { _ in } + ) -> TextEditorView { + TextEditorView( + title: title, + isRequired: isRequired, + placeholder: placeholder, + text: text, + maxCharacters: maxCharacters, + minHeight: 120, + icon: icon, + isFocused: isFocused, + onFocusChange: onFocusChange + ) + } +} + +#Preview { + VStack(spacing: 16) { + TextEditorView.description( + title: "描述", + isRequired: true, + placeholder: "请输入描述内容...", + text: .constant(""), + icon: "text.quote" + ) + + TextEditorView.longText( + title: "长文本", + isRequired: false, + placeholder: "请输入长文本内容...", + text: .constant(""), + icon: "doc.text" + ) + + TextEditorView.emailBody( + title: "邮件正文", + isRequired: true, + placeholder: "输入邮件正文内容...", + text: .constant(""), + icon: "envelope" + ) + } + .padding() +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/TextInputView.swift b/MyQrCode/Views/Components/TextInputView.swift new file mode 100644 index 0000000..eb2b6a0 --- /dev/null +++ b/MyQrCode/Views/Components/TextInputView.swift @@ -0,0 +1,110 @@ +import SwiftUI + +// MARK: - 通用文本输入组件 +struct TextInputView: View { + @Binding var content: String + @FocusState var isContentFieldFocused: Bool + let placeholder: String + let maxCharacters: Int + + var body: some View { + VStack(spacing: 8) { + ZStack { + // 输入框主体 + TextEditor(text: $content) + .frame(minHeight: 120) + .padding(8) + .background(Color(.systemBackground)) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(isContentFieldFocused ? Color.blue : Color(.systemGray4), lineWidth: 1) + ) + .focused($isContentFieldFocused) + .onChange(of: content) { newValue in + // 限制最大字符数 + if newValue.count > maxCharacters { + content = String(newValue.prefix(maxCharacters)) + } + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("完成") { + isContentFieldFocused = false + } + .foregroundColor(.blue) + .font(.system(size: 16, weight: .medium)) + } + } + + // 占位符文本 - 左上角对齐 + if content.isEmpty && !isContentFieldFocused { + VStack { + HStack { + Text(placeholder) + .foregroundColor(.secondary) + .font(.body) + Spacer() + } + Spacer() + } + .padding(16) + .allowsHitTesting(false) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } + } + + // 字符计数和限制提示 - 输入框底下 + HStack { + Spacer() // Pushes content to the right + + VStack(alignment: .trailing, spacing: 4) { + // 字符限制提示 + if content.count >= maxCharacters { + HStack(spacing: 4) { + Image(systemName: "exclamationmark.triangle") + .font(.caption) + .foregroundColor(.orange) + Text("已达到最大字符数") + .font(.caption) + .foregroundColor(.orange) + } + } else if content.count >= Int(Double(maxCharacters) * 0.93) { + HStack(spacing: 4) { + Image(systemName: "info.circle") + .font(.caption) + .foregroundColor(.blue) + Text("接近字符限制") + .font(.caption) + .foregroundColor(.blue) + } + } + + // 字符计数 + Text("\(content.count)/\(maxCharacters)") + .font(.caption) + .foregroundColor(getCharacterCountColor()) + } + } + } + } + + private func getCharacterCountColor() -> Color { + if content.count >= maxCharacters { + return .orange + } else if content.count >= Int(Double(maxCharacters) * 0.93) { + return .blue + } else { + return .secondary + } + } +} + +#Preview { + TextInputView( + content: .constant(""), + placeholder: "输入任意文本内容...", + maxCharacters: 150 + ) +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/URLInputView.swift b/MyQrCode/Views/Components/URLInputView.swift new file mode 100644 index 0000000..0fa5948 --- /dev/null +++ b/MyQrCode/Views/Components/URLInputView.swift @@ -0,0 +1,102 @@ +import SwiftUI + +// MARK: - URL输入组件 +struct URLInputView: View { + @Binding var url: String + @FocusState var isUrlFieldFocused: Bool + + var body: some View { + VStack(spacing: 16) { + // URL输入框 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("网址") + .font(.subheadline) + .foregroundColor(.primary) + Text("*") + .foregroundColor(.red) + Spacer() + } + + TextField("https://www.example.com", text: $url) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .keyboardType(.URL) + .autocapitalization(.none) + .focused($isUrlFieldFocused) + .onChange(of: url) { newValue in + // 自动添加https://前缀 + if !newValue.isEmpty && !newValue.hasPrefix("http://") && !newValue.hasPrefix("https://") { + url = "https://" + newValue + } + } + } + + // 格式说明 + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: "info.circle") + .font(.caption) + .foregroundColor(.blue) + + Text("格式说明") + .font(.caption) + .foregroundColor(.primary) + + Spacer() + } + + Text("• 可以输入完整URL:https://www.example.com\n• 或输入域名:www.example.com\n• 系统会自动添加https://前缀") + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(nil) + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color.blue.opacity(0.1)) + ) + + // 预览URL + if !url.isEmpty { + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: "link") + .font(.caption) + .foregroundColor(.green) + + Text("预览URL") + .font(.caption) + .foregroundColor(.primary) + + Spacer() + } + + Text(url) + .font(.caption) + .foregroundColor(.green) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(Color.green.opacity(0.1)) + ) + } + } + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("完成") { + isUrlFieldFocused = false + } + .foregroundColor(.blue) + .font(.system(size: 16, weight: .medium)) + } + } + } +} + +#Preview { + URLInputView(url: .constant("")) +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/UtilityFunctions.swift b/MyQrCode/Views/Components/UtilityFunctions.swift new file mode 100644 index 0000000..1142015 --- /dev/null +++ b/MyQrCode/Views/Components/UtilityFunctions.swift @@ -0,0 +1,390 @@ +import SwiftUI +import Foundation + +// MARK: - 通用工具函数 + +// MARK: - 字符串扩展 +extension String { + /// 检查字符串是否为有效的邮箱地址 + var isValidEmail: Bool { + let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex) + return emailPredicate.evaluate(with: self) + } + + /// 检查字符串是否为有效的电话号码 + var isValidPhone: Bool { + let phoneRegex = "^[+]?[0-9\\s\\-\\(\\)]{7,}$" + let phonePredicate = NSPredicate(format: "SELF MATCHES %@", phoneRegex) + return phonePredicate.evaluate(with: self) + } + + /// 检查字符串是否为有效的URL + var isValidURL: Bool { + guard let url = URL(string: self) else { return false } + return UIApplication.shared.canOpenURL(url) + } + + /// 移除字符串两端的空白字符 + var trimmed: String { + self.trimmingCharacters(in: .whitespacesAndNewlines) + } + + /// 检查字符串是否为空或只包含空白字符 + var isEmptyOrWhitespace: Bool { + self.trimmed.isEmpty + } + + /// 获取字符串的字符数(不包括空白字符) + var characterCount: Int { + self.trimmed.count + } + + /// 截取字符串到指定长度 + func truncated(to length: Int, suffix: String = "...") -> String { + if self.count <= length { + return self + } + let index = self.index(self.startIndex, offsetBy: length - suffix.count) + return String(self[.. String { + if self.hasPrefix("http://") || self.hasPrefix("https://") { + return self + } + return "https://" + self + } +} + +// MARK: - 日期扩展 +extension Date { + /// 格式化日期为字符串 + func formattedString(style: DateFormatter.Style = .medium) -> String { + let formatter = DateFormatter() + formatter.dateStyle = style + formatter.locale = Locale(identifier: "zh_CN") + return formatter.string(from: self) + } + + /// 格式化日期为时间字符串 + func formattedTimeString() -> String { + let formatter = DateFormatter() + formatter.timeStyle = .short + formatter.locale = Locale(identifier: "zh_CN") + return formatter.string(from: self) + } + + /// 格式化日期为完整字符串 + func formattedFullString() -> String { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .short + formatter.locale = Locale(identifier: "zh_CN") + return formatter.string(from: self) + } + + /// 检查日期是否为今天 + var isToday: Bool { + Calendar.current.isDateInToday(self) + } + + /// 检查日期是否为昨天 + var isYesterday: Bool { + Calendar.current.isDateInYesterday(self) + } + + /// 获取相对时间描述 + var relativeTimeDescription: String { + let now = Date() + let components = Calendar.current.dateComponents([.minute, .hour, .day], from: self, to: now) + + if let day = components.day, day > 0 { + if day == 1 { + return "昨天" + } else if day < 7 { + return "\(day)天前" + } else { + return self.formattedString(style: .short) + } + } else if let hour = components.hour, hour > 0 { + return "\(hour)小时前" + } else if let minute = components.minute, minute > 0 { + return "\(minute)分钟前" + } else { + return "刚刚" + } + } +} + +// MARK: - 颜色扩展 +extension Color { + /// 创建随机颜色 + static func random() -> Color { + Color( + red: Double.random(in: 0...1), + green: Double.random(in: 0...1), + blue: Double.random(in: 0...1) + ) + } + + /// 创建系统颜色 + static let systemBackground = Color(.systemBackground) + static let systemGroupedBackground = Color(.systemGroupedBackground) + + /// 创建语义颜色 + static let label = Color(.label) + static let secondaryLabel = Color(.secondaryLabel) + static let tertiaryLabel = Color(.tertiaryLabel) + static let quaternaryLabel = Color(.quaternaryLabel) + + /// 创建系统颜色 + static let systemBlue = Color(.systemBlue) + static let systemGreen = Color(.systemGreen) + static let systemIndigo = Color(.systemIndigo) + static let systemOrange = Color(.systemOrange) + static let systemPink = Color(.systemPink) + static let systemPurple = Color(.systemPurple) + static let systemRed = Color(.systemRed) + static let systemTeal = Color(.systemTeal) + static let systemYellow = Color(.systemYellow) + + /// 创建系统灰色 + static let systemGray = Color(.systemGray) + static let systemGray2 = Color(.systemGray2) + static let systemGray3 = Color(.systemGray3) + static let systemGray4 = Color(.systemGray4) + static let systemGray5 = Color(.systemGray5) + static let systemGray6 = Color(.systemGray6) +} + +// MARK: - 视图扩展 +extension View { + /// 添加圆角 + func roundedCorners(_ radius: CGFloat, corners: UIRectCorner = .allCorners) -> some View { + clipShape(RoundedCorner(radius: radius, corners: corners)) + } + + /// 添加自定义阴影 + func customShadow( + color: Color = .black.opacity(0.1), + radius: CGFloat = 8, + x: CGFloat = 0, + y: CGFloat = 4 + ) -> some View { + self.shadow(color: color, radius: radius, x: x, y: y) + } + + /// 添加边框 + func customBorder( + _ color: Color, + width: CGFloat = 1, + cornerRadius: CGFloat = 0 + ) -> some View { + self.overlay( + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(color, lineWidth: width) + ) + } + + /// 添加自定义背景色 + func customBackground(_ color: Color, cornerRadius: CGFloat = 0) -> some View { + self.background( + RoundedRectangle(cornerRadius: cornerRadius) + .fill(color) + ) + } + + /// 隐藏键盘 + func hideKeyboard() { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } +} + +// MARK: - 自定义圆角形状 +struct RoundedCorner: Shape { + var radius: CGFloat = .infinity + var corners: UIRectCorner = .allCorners + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius) + ) + return Path(path.cgPath) + } +} + +// MARK: - 通用验证函数 +struct ValidationHelper { + /// 验证邮箱地址 + static func isValidEmail(_ email: String) -> Bool { + email.isValidEmail + } + + /// 验证电话号码 + static func isValidPhone(_ phone: String) -> Bool { + phone.isValidPhone + } + + /// 验证URL + static func isValidURL(_ url: String) -> Bool { + url.isValidURL + } + + /// 验证必填字段 + static func isRequiredFieldValid(_ field: String) -> Bool { + !field.isEmptyOrWhitespace + } + + /// 验证字符长度 + static func isLengthValid(_ text: String, min: Int, max: Int) -> Bool { + let count = text.characterCount + return count >= min && count <= max + } + + /// 验证密码强度 + static func getPasswordStrength(_ password: String) -> PasswordStrength { + var score = 0 + + if password.count >= 8 { score += 1 } + if password.range(of: "[a-z]", options: .regularExpression) != nil { score += 1 } + if password.range(of: "[A-Z]", options: .regularExpression) != nil { score += 1 } + if password.range(of: "[0-9]", options: .regularExpression) != nil { score += 1 } + if password.range(of: "[^a-zA-Z0-9]", options: .regularExpression) != nil { score += 1 } + + switch score { + case 0...1: + return .weak + case 2...3: + return .medium + case 4...5: + return .strong + default: + return .weak + } + } +} + +// MARK: - 密码强度枚举 +enum PasswordStrength { + case weak + case medium + case strong + + var description: String { + switch self { + case .weak: + return "弱" + case .medium: + return "中" + case .strong: + return "强" + } + } + + var color: Color { + switch self { + case .weak: + return .red + case .medium: + return .orange + case .strong: + return .green + } + } +} + +// MARK: - 通用格式化函数 +struct FormatHelper { + /// 格式化文件大小 + static func formatFileSize(_ bytes: Int64) -> String { + let formatter = ByteCountFormatter() + formatter.allowedUnits = [.useKB, .useMB, .useGB] + formatter.countStyle = .file + return formatter.string(fromByteCount: bytes) + } + + /// 格式化数字 + static func formatNumber(_ number: Int) -> String { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + return formatter.string(from: NSNumber(value: number)) ?? "\(number)" + } + + /// 格式化百分比 + static func formatPercentage(_ value: Double) -> String { + let formatter = NumberFormatter() + formatter.numberStyle = .percent + formatter.minimumFractionDigits = 1 + formatter.maximumFractionDigits = 1 + return formatter.string(from: NSNumber(value: value)) ?? "\(value * 100)%" + } + + /// 格式化货币 + static func formatCurrency(_ amount: Double, locale: Locale = .current) -> String { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.locale = locale + return formatter.string(from: NSNumber(value: amount)) ?? "\(amount)" + } +} + +// MARK: - 通用动画函数 +struct AnimationHelper { + /// 弹性动画 + static let spring = Animation.spring(response: 0.5, dampingFraction: 0.8, blendDuration: 0) + + /// 缓入动画 + static let easeIn = Animation.easeIn(duration: 0.3) + + /// 缓出动画 + static let easeOut = Animation.easeOut(duration: 0.3) + + /// 缓入缓出动画 + static let easeInOut = Animation.easeInOut(duration: 0.3) + + /// 线性动画 + static let linear = Animation.linear(duration: 0.3) +} + +// MARK: - 通用反馈函数 +struct FeedbackHelper { + /// 轻触反馈 + static func lightImpact() { + let impactFeedback = UIImpactFeedbackGenerator(style: .light) + impactFeedback.impactOccurred() + } + + /// 中等触反馈 + static func mediumImpact() { + let impactFeedback = UIImpactFeedbackGenerator(style: .medium) + impactFeedback.impactOccurred() + } + + /// 重触反馈 + static func heavyImpact() { + let impactFeedback = UIImpactFeedbackGenerator(style: .heavy) + impactFeedback.impactOccurred() + } + + /// 成功反馈 + static func success() { + let notificationFeedback = UINotificationFeedbackGenerator() + notificationFeedback.notificationOccurred(.success) + } + + /// 警告反馈 + static func warning() { + let notificationFeedback = UINotificationFeedbackGenerator() + notificationFeedback.notificationOccurred(.warning) + } + + /// 错误反馈 + static func error() { + let notificationFeedback = UINotificationFeedbackGenerator() + notificationFeedback.notificationOccurred(.error) + } +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/ValidationView.swift b/MyQrCode/Views/Components/ValidationView.swift new file mode 100644 index 0000000..858a663 --- /dev/null +++ b/MyQrCode/Views/Components/ValidationView.swift @@ -0,0 +1,237 @@ +import SwiftUI + +// MARK: - 通用验证组件 +struct ValidationView: View { + let validationState: ValidationState + let message: String? + let showIcon: Bool + + init( + validationState: ValidationState, + message: String? = nil, + showIcon: Bool = true + ) { + self.validationState = validationState + self.message = message + self.showIcon = showIcon + } + + var body: some View { + if validationState != .none { + HStack(spacing: 6) { + if showIcon { + Image(systemName: iconName) + .font(.caption) + .foregroundColor(iconColor) + } + + if let message = message { + Text(message) + .font(.caption) + .foregroundColor(textColor) + } + + Spacer() + } + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(backgroundColor) + ) + } + } + + private var iconName: String { + switch validationState { + case .none: + return "" + case .valid: + return "checkmark.circle" + case .warning: + return "exclamationmark.triangle" + case .error: + return "xmark.circle" + case .info: + return "info.circle" + } + } + + private var iconColor: Color { + switch validationState { + case .none: + return .clear + case .valid: + return .green + case .warning: + return .orange + case .error: + return .red + case .info: + return .blue + } + } + + private var textColor: Color { + switch validationState { + case .none: + return .clear + case .valid: + return .green + case .warning: + return .orange + case .error: + return .red + case .info: + return .blue + } + } + + private var backgroundColor: Color { + switch validationState { + case .none: + return .clear + case .valid: + return .green.opacity(0.1) + case .warning: + return .orange.opacity(0.1) + case .error: + return .red.opacity(0.1) + case .info: + return .blue.opacity(0.1) + } + } +} + +// MARK: - 验证状态枚举 +enum ValidationState { + case none + case valid + case warning + case error + case info +} + +// MARK: - 预定义的验证样式 +extension ValidationView { + static func success(message: String, showIcon: Bool = true) -> ValidationView { + ValidationView( + validationState: .valid, + message: message, + showIcon: showIcon + ) + } + + static func warning(message: String, showIcon: Bool = true) -> ValidationView { + ValidationView( + validationState: .warning, + message: message, + showIcon: showIcon + ) + } + + static func error(message: String, showIcon: Bool = true) -> ValidationView { + ValidationView( + validationState: .error, + message: message, + showIcon: showIcon + ) + } + + static func info(message: String, showIcon: Bool = true) -> ValidationView { + ValidationView( + validationState: .info, + message: message, + showIcon: showIcon + ) + } +} + +// MARK: - 字符计数验证 +struct CharacterCountValidation: View { + let currentCount: Int + let maxCount: Int + let warningThreshold: Double + + init( + currentCount: Int, + maxCount: Int, + warningThreshold: Double = 0.9 + ) { + self.currentCount = currentCount + self.maxCount = maxCount + self.warningThreshold = warningThreshold + } + + var body: some View { + HStack { + Spacer() + + if currentCount >= maxCount { + ValidationView.error(message: "已达到最大字符数") + } else if currentCount >= Int(Double(maxCount) * warningThreshold) { + ValidationView.warning(message: "接近字符限制") + } else { + Text("\(currentCount)/\(maxCount)") + .font(.caption) + .foregroundColor(.secondary) + } + } + } +} + +// MARK: - 必填字段验证 +struct RequiredFieldValidation: View { + let fieldName: String + let isEmpty: Bool + + var body: some View { + if isEmpty { + ValidationView.error(message: "\(fieldName)为必填项") + } + } +} + +// MARK: - 格式验证 +struct FormatValidation: View { + let fieldName: String + let isValid: Bool + let errorMessage: String? + + init( + fieldName: String, + isValid: Bool, + errorMessage: String? = nil + ) { + self.fieldName = fieldName + self.isValid = isValid + self.errorMessage = errorMessage + } + + var body: some View { + if !isValid { + ValidationView.error( + message: errorMessage ?? "\(fieldName)格式不正确" + ) + } + } +} + +#Preview { + VStack(spacing: 16) { + ValidationView.success(message: "验证成功!") + ValidationView.warning(message: "这是一个警告") + ValidationView.error(message: "这是一个错误") + ValidationView.info(message: "这是一个提示") + + CharacterCountValidation(currentCount: 95, maxCount: 100) + CharacterCountValidation(currentCount: 100, maxCount: 100) + + RequiredFieldValidation(fieldName: "用户名", isEmpty: true) + RequiredFieldValidation(fieldName: "邮箱", isEmpty: false) + + FormatValidation(fieldName: "邮箱地址", isValid: false) + FormatValidation(fieldName: "电话号码", isValid: true) + } + .padding() +} \ No newline at end of file diff --git a/MyQrCode/Views/Components/WiFiInputView.swift b/MyQrCode/Views/Components/WiFiInputView.swift new file mode 100644 index 0000000..dc773a4 --- /dev/null +++ b/MyQrCode/Views/Components/WiFiInputView.swift @@ -0,0 +1,128 @@ +import SwiftUI + +// MARK: - WiFi输入组件 +struct WiFiInputView: View { + @Binding var ssid: String + @Binding var password: String + @Binding var encryptionType: WiFiEncryptionType + @FocusState var focusedField: WiFiField? + + // WiFi字段枚举 + enum WiFiField: Hashable { + case ssid, password + } + + // WiFi加密类型 + enum WiFiEncryptionType: String, CaseIterable { + case none = "None" + case wep = "WEP" + case wpa = "WPA" + case wpa2 = "WPA2" + case wpa3 = "WPA3" + + var displayName: String { + switch self { + case .none: return "无加密" + case .wep: return "WEP" + case .wpa: return "WPA" + case .wpa2: return "WPA2" + case .wpa3: return "WPA3" + } + } + } + + var body: some View { + VStack(spacing: 16) { + // SSID (必填) + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("网络名称 (SSID)") + .font(.subheadline) + .foregroundColor(.primary) + Text("*") + .foregroundColor(.red) + Spacer() + } + + TextField("MyWiFi", text: $ssid) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .focused($focusedField, equals: .ssid) + } + + // 密码 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("密码") + .font(.subheadline) + .foregroundColor(.primary) + Spacer() + } + + SecureField("WiFi密码", text: $password) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .focused($focusedField, equals: .password) + } + + // 加密类型 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("加密类型") + .font(.subheadline) + .foregroundColor(.primary) + Spacer() + } + + Picker("加密类型", selection: $encryptionType) { + ForEach(WiFiEncryptionType.allCases, id: \.self) { type in + Text(type.displayName).tag(type) + } + } + .pickerStyle(SegmentedPickerStyle()) + } + + // 格式说明 + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: "info.circle") + .font(.caption) + .foregroundColor(.blue) + + Text("格式说明") + .font(.caption) + .foregroundColor(.primary) + + Spacer() + } + + Text("• 网络名称(SSID)为必填项\n• 密码为可选项,无加密时可留空\n• 将生成标准WiFi连接格式") + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(nil) + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color.blue.opacity(0.1)) + ) + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("完成") { + focusedField = nil + } + .foregroundColor(.blue) + .font(.system(size: 16, weight: .medium)) + } + } + } +} + +#Preview { + WiFiInputView( + ssid: .constant(""), + password: .constant(""), + encryptionType: .constant(.wpa2) + ) +} \ No newline at end of file diff --git a/MyQrCode/Views/CreateQRCodeView.swift.backup b/MyQrCode/Views/CreateQRCodeView.swift.backup deleted file mode 100644 index 3d39b95..0000000 --- a/MyQrCode/Views/CreateQRCodeView.swift.backup +++ /dev/null @@ -1,642 +0,0 @@ -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 = "" - @State private var showingAlert = false - @State private var alertMessage = "" - - // 输入焦点,确保进入页面自动弹出键盘 - @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: EmailField? - - // Email字段枚举 - private enum EmailField: Hashable { - case address, subject, body, cc, bcc - } - - 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 { - // 稍延迟以确保进入页面时自动聚焦 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - isContentFieldFocused = true - } - } - .onTapGesture { - // 点击外部关闭键盘 - isContentFieldFocused = false - } - } - - // MARK: - UI Components - - private var emailInputSection: some View { - VStack(spacing: 16) { - // Email地址 (必填) - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("邮箱地址") - .font(.subheadline) - .foregroundColor(.primary) - Text("*") - .foregroundColor(.red) - Spacer() - } - - TextField("user@example.com", text: $emailAddress) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .keyboardType(.emailAddress) - .autocapitalization(.none) - .focused($focusedEmailField, equals: .address) - } - - // 主题 (必填) - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("主题") - .font(.subheadline) - .foregroundColor(.primary) - Text("*") - .foregroundColor(.red) - Spacer() - } - - TextField("邮件主题", text: $emailSubject) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .focused($focusedEmailField, equals: .subject) - } - - // 正文 (必填) - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("正文") - .font(.subheadline) - .foregroundColor(.primary) - Text("*") - .foregroundColor(.red) - Spacer() - } - - ZStack { - TextEditor(text: $emailBody) - .frame(minHeight: 120) - .padding(8) - .background(Color(.systemBackground)) - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(focusedEmailField == .body ? Color.blue : Color(.systemGray4), lineWidth: 1) - ) - .focused($focusedEmailField, equals: .body) - .onChange(of: emailBody) { newValue in - // 限制最大字符数为1200 - if newValue.count > 1200 { - emailBody = String(newValue.prefix(1200)) - } - } - - // 占位符文本 - if emailBody.isEmpty && focusedEmailField != .body { - VStack { - HStack { - Text("输入邮件正文内容...") - .foregroundColor(.secondary) - .font(.body) - Spacer() - } - Spacer() - } - .padding(16) - .allowsHitTesting(false) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - } - } - - // 字符计数 - HStack { - Spacer() - Text("\(emailBody.count)/1200") - .font(.caption) - .foregroundColor(emailBody.count >= 1200 ? .orange : .secondary) - } - } - - // CC地址 (可选) - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("抄送地址") - .font(.subheadline) - .foregroundColor(.primary) - Spacer() - } - - TextField("cc@example.com", text: $emailCc) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .keyboardType(.emailAddress) - .autocapitalization(.none) - .focused($focusedEmailField, equals: .cc) - } - - // BCC地址 (可选) - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("密送地址") - .font(.subheadline) - .foregroundColor(.primary) - Spacer() - } - - TextField("bcc@example.com", text: $emailBcc) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .keyboardType(.emailAddress) - .autocapitalization(.none) - .focused($focusedEmailField, equals: .bcc) - } - } - .toolbar { - ToolbarItemGroup(placement: .keyboard) { - Spacer() - Button("完成") { - focusedEmailField = nil - } - .foregroundColor(.blue) - .font(.system(size: 16, weight: .medium)) - } - } - } - - private var inputAndPreviewSection: some View { - ScrollView { - VStack(spacing: 24) { - // 输入提示 - VStack(spacing: 12) { - HStack { - Image(systemName: "info.circle") - .font(.caption) - .foregroundColor(.blue) - - Text(getContentHint()) - .font(.caption) - .foregroundColor(.secondary) - .lineLimit(nil) - - Spacer() - } - .padding(.horizontal, 12) - .padding(.vertical, 8) - .background( - RoundedRectangle(cornerRadius: 8) - .fill(Color.blue.opacity(0.1)) - ) - } - .padding(.horizontal, 20) - - // 内容输入区域 - VStack(spacing: 16) { - HStack { - Text(selectedQRCodeType == .mail ? "邮件信息" : "输入内容") - .font(.headline) - .foregroundColor(.primary) - - Spacer() - } - - if selectedQRCodeType == .mail { - // Email专用输入界面 - emailInputSection - } else { - // 通用输入界面 - VStack(spacing: 8) { - ZStack { - // 输入框主体 - TextEditor(text: $content) - .frame(minHeight: 120) - .padding(8) - .background(Color(.systemBackground)) - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(isContentFieldFocused ? Color.blue : Color(.systemGray4), lineWidth: 1) - ) - .focused($isContentFieldFocused) - .onChange(of: content) { newValue in - // 限制最大字符数为150 - if newValue.count > 150 { - content = String(newValue.prefix(150)) - } - } - .toolbar { - ToolbarItemGroup(placement: .keyboard) { - Spacer() - Button("完成") { - isContentFieldFocused = false - } - .foregroundColor(.blue) - .font(.system(size: 16, weight: .medium)) - } - } - - // 占位符文本 - 左上角对齐 - if content.isEmpty && !isContentFieldFocused { - VStack { - HStack { - Text(getPlaceholderText()) - .foregroundColor(.secondary) - .font(.body) - Spacer() - } - Spacer() - } - .padding(16) - .allowsHitTesting(false) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - } - } - - // 字符计数和限制提示 - 输入框底下 - HStack { - Spacer() - - VStack(alignment: .trailing, spacing: 4) { - // 字符限制提示 - if content.count >= 150 { - HStack(spacing: 4) { - Image(systemName: "exclamationmark.triangle") - .font(.caption) - .foregroundColor(.orange) - - Text("已达到最大字符数") - .font(.caption) - .foregroundColor(.orange) - } - } else if content.count >= 140 { - HStack(spacing: 4) { - Image(systemName: "info.circle") - .font(.caption) - .foregroundColor(.blue) - - Text("接近字符限制") - .font(.caption) - .foregroundColor(.blue) - } - } - - // 字符计数 - Text("\(content.count)/150") - .font(.caption) - .foregroundColor(getCharacterCountColor()) - } - } - } - } - .padding(.horizontal, 20) - - // 预览区域 - if !content.isEmpty { - VStack(spacing: 16) { - HStack { - Text("预览") - .font(.headline) - .foregroundColor(.primary) - - Spacer() - - Button(action: { - // 可以添加分享功能 - }) { - Image(systemName: "square.and.arrow.up") - .font(.system(size: 16)) - .foregroundColor(.blue) - } - } - - VStack(spacing: 16) { - // 二维码图片 - if let qrCodeImage = generateQRCodeImage() { - Image(uiImage: qrCodeImage) - .interpolation(.none) - .resizable() - .scaledToFit() - .frame(width: 200, height: 200) - .background(Color.white) - .cornerRadius(12) - .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4) - } - - // 内容预览卡片 - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("内容") - .font(.caption) - .foregroundColor(.secondary) - - Spacer() - - Text(selectedQRCodeType.displayName) - .font(.caption) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.orange.opacity(0.1)) - .foregroundColor(.orange) - .cornerRadius(4) - } - - Text(formatContentForQRCodeType()) - .font(.body) - .foregroundColor(.primary) - .textSelection(.enabled) - } - .padding() - .background(Color(.systemGray6)) - .cornerRadius(8) - } - .padding() - .background(Color(.systemBackground)) - .cornerRadius(12) - .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 2) - } - .padding(.horizontal, 20) - } - - Spacer(minLength: 100) - } - .padding(.top, 20) - } - .background(Color(.systemGroupedBackground)) - } - - - - // MARK: - Helper Methods - - private func canCreateQRCode() -> Bool { - switch selectedQRCodeType { - case .mail: - // Email类型:邮箱地址、主题、正文为必填 - return !emailAddress.isEmpty && !emailSubject.isEmpty && !emailBody.isEmpty - default: - // 其他类型:内容不能为空 - return !content.isEmpty - } - } - - private func getCharacterCountColor() -> Color { - if content.count >= 150 { - return .orange - } else if content.count >= 140 { - return .blue - } else { - return .secondary - } - } - - private func getContentHint() -> String { - switch selectedQRCodeType { - case .text: - return "输入任意文本内容" - case .url: - return "输入网址,如:https://www.example.com" - case .mail: - return "输入邮箱地址,如:user@example.com" - case .phone: - return "输入电话号码,如:+86 138 0013 8000" - case .sms: - return "输入短信内容,如:Hello World" - case .wifi: - return "输入WiFi信息,如:SSID:MyWiFi,Password:12345678" - case .vcard: - return "输入联系人信息" - case .mecard: - return "输入联系人信息(简化版)" - case .location: - return "输入地理位置,如:40.7128,-74.0060" - case .calendar: - return "输入日历事件信息" - case .instagram: - return "输入Instagram用户名或链接" - case .facebook: - return "输入Facebook用户名或链接" - case .spotify: - return "输入Spotify歌曲或播放列表链接" - case .twitter: - return "输入Twitter用户名或链接" - case .whatsapp: - return "输入WhatsApp消息内容" - case .viber: - return "输入Viber消息内容" - case .snapchat: - return "输入Snapchat用户名" - case .tiktok: - return "输入TikTok用户名或链接" - } - } - - private func getPlaceholderText() -> String { - switch selectedQRCodeType { - case .text: - return "输入文本内容" - case .url: - return "输入网址" - case .mail: - return "输入邮箱地址" - case .phone: - return "输入电话号码" - case .sms: - return "输入短信内容" - case .wifi: - return "输入WiFi信息" - case .vcard: - return "输入联系人信息" - case .mecard: - return "输入联系人信息" - case .location: - return "输入地理位置坐标" - case .calendar: - return "输入日历事件信息" - case .instagram: - return "输入Instagram信息" - case .facebook: - return "输入Facebook信息" - case .spotify: - return "输入Spotify链接" - case .twitter: - return "输入Twitter信息" - case .whatsapp: - return "输入WhatsApp消息" - case .viber: - return "输入Viber消息" - case .snapchat: - return "输入Snapchat用户名" - case .tiktok: - return "输入TikTok信息" - } - } - - private func generateQRCodeImage() -> UIImage? { - // 根据二维码类型检查内容是否为空 - let hasContent: Bool - switch selectedQRCodeType { - case .mail: - hasContent = !emailAddress.isEmpty && !emailSubject.isEmpty && !emailBody.isEmpty - default: - hasContent = !content.isEmpty - } - - guard hasContent 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 } - - // 转换为UIImage - 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 content.hasPrefix("http") ? content : "https://\(content)" - 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:\(content)" - case .sms: - return "sms:\(content)" - case .wifi: - return "WIFI:T:WPA;S:\(content);P:password;;" - case .vcard: - return "BEGIN:VCARD\nVERSION:3.0\nFN:\(content)\nEND:VCARD" - case .mecard: - return "MECARD:N:\(content);;" - case .location: - return "geo:\(content)" - case .calendar: - return "BEGIN:VEVENT\nSUMMARY:\(content)\nEND:VEVENT" - case .instagram: - return "https://instagram.com/\(content)" - case .facebook: - return "https://facebook.com/\(content)" - case .spotify: - return content.hasPrefix("http") ? content : "https://open.spotify.com/track/\(content)" - case .twitter: - return "https://twitter.com/\(content)" - case .whatsapp: - return "https://wa.me/\(content)" - case .viber: - return "viber://chat?number=\(content)" - case .snapchat: - return "https://snapchat.com/add/\(content)" - case .tiktok: - return "https://tiktok.com/@\(content)" - } - } - - 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: - historyItem.content = "邮箱: \(emailAddress)\n主题: \(emailSubject)\n正文: \(emailBody)" - if !emailCc.isEmpty { - historyItem.content += "\n抄送: \(emailCc)" - } - if !emailBcc.isEmpty { - historyItem.content += "\n密送: \(emailBcc)" - } - 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: .text) - } -} \ No newline at end of file