Add DESCRIPTION field and time formatting to QRCodeParser for enhanced calendar event parsing

main
v504 2 months ago
parent d5fef06e76
commit 12764a2598

@ -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 ?? ""

@ -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("")
)
}

@ -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<Content: View>(
@ViewBuilder content: () -> Content
) -> SimpleCardView {
SimpleCardView(content: content)
}
static func elevated<Content: View>(
@ViewBuilder content: () -> Content
) -> SimpleCardView {
SimpleCardView(
shadowColor: .black.opacity(0.15),
shadowRadius: 12,
shadowOffset: CGSize(width: 0, height: 6),
content: content
)
}
static func subtle<Content: View>(
@ViewBuilder content: () -> Content
) -> SimpleCardView {
SimpleCardView(
shadowColor: .black.opacity(0.05),
shadowRadius: 4,
shadowOffset: CGSize(width: 0, height: 2),
content: content
)
}
static func compact<Content: View>(
@ViewBuilder content: () -> Content
) -> SimpleCardView {
SimpleCardView(
padding: EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12),
cornerRadius: 8,
content: content
)
}
static func large<Content: View>(
@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()
}

@ -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("")
)
}

@ -0,0 +1,141 @@
import SwiftUI
// MARK: -
struct DatePickerView: View {
let title: String
let isRequired: Bool
let date: Binding<Date>
let displayedComponents: DatePickerComponents
let icon: String?
init(
title: String,
isRequired: Bool = false,
date: Binding<Date>,
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<Date>,
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<Date>,
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<Date>,
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<Date>,
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<Date>,
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()
}

@ -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("")
)
}

@ -0,0 +1,285 @@
import SwiftUI
// MARK: -
struct FormView<Content: View>: 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<Content: View>: 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<Content: View>: 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: {})
}
}
}

@ -0,0 +1,217 @@
import SwiftUI
// MARK: -
struct InputComponentFactory {
// QR
static func createInputComponent(
for qrCodeType: QRCodeType,
content: Binding<String>,
emailAddress: Binding<String>,
emailSubject: Binding<String>,
emailBody: Binding<String>,
emailCc: Binding<String>,
emailBcc: Binding<String>,
focusedEmailField: FocusState<EmailInputView.EmailField?>,
isContentFieldFocused: FocusState<Bool>,
ssid: Binding<String>,
password: Binding<String>,
encryptionType: Binding<WiFiInputView.WiFiEncryptionType>,
focusedWiFiField: FocusState<WiFiInputView.WiFiField?>,
firstName: Binding<String>,
lastName: Binding<String>,
phone: Binding<String>,
email: Binding<String>,
company: Binding<String>,
title: Binding<String>,
address: Binding<String>,
website: Binding<String>,
focusedContactField: FocusState<ContactInputView.ContactField?>,
latitude: Binding<String>,
longitude: Binding<String>,
locationName: Binding<String>,
focusedLocationField: FocusState<LocationInputView.LocationField?>,
eventTitle: Binding<String>,
eventDescription: Binding<String>,
startDate: Binding<Date>,
endDate: Binding<Date>,
location: Binding<String>,
focusedCalendarField: FocusState<CalendarInputView.CalendarField?>,
username: Binding<String>,
message: Binding<String>,
focusedSocialField: FocusState<SocialInputView.SocialField?>,
phoneNumber: Binding<String>,
phoneMessage: Binding<String>,
focusedPhoneField: FocusState<PhoneInputView.PhoneField?>,
url: Binding<String>,
isUrlFieldFocused: FocusState<Bool>
) -> 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
}
}
}

@ -0,0 +1,208 @@
import SwiftUI
// MARK: -
struct InputFieldView: View {
let title: String
let isRequired: Bool
let placeholder: String
let text: Binding<String>
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<String>,
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<String>,
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<String>,
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<String>,
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<String>,
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<String>,
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()
}

@ -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()
}

@ -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()
}

@ -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<String>,
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<String>,
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<String>,
onDone: @escaping () -> Void = {}
) -> KeyboardToolbarView {
KeyboardToolbarView(
onDone: onDone,
additionalButtons: [
.clear(text: text)
]
)
}
static func withCopyPaste(
text: Binding<String>,
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()
}

@ -0,0 +1,318 @@
import SwiftUI
// MARK: -
struct ListView<Data: RandomAccessCollection, Content: View>: 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<Content: View>: 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<U: View>(
onTap: (() -> Void)? = nil,
@ViewBuilder content: () -> U
) -> ListItem<U> {
ListItem<U>(onTap: onTap, content: content)
}
static func compact<U: View>(
onTap: (() -> Void)? = nil,
@ViewBuilder content: () -> U
) -> ListItem<U> {
ListItem<U>(
padding: EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12),
cornerRadius: 8,
onTap: onTap,
content: content
)
}
static func elevated<U: View>(
onTap: (() -> Void)? = nil,
@ViewBuilder content: () -> U
) -> ListItem<U> {
ListItem<U>(
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<AnyView>.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
}

@ -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("")
)
}

@ -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
)
}

@ -0,0 +1,108 @@
import SwiftUI
// MARK: -
struct SimplePickerView<T: Hashable>: View {
let title: String
let isRequired: Bool
let selection: Binding<T>
let options: [T]
let optionTitle: (T) -> String
let icon: String?
init(
title: String,
isRequired: Bool = false,
selection: Binding<T>,
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<WiFiInputView.WiFiEncryptionType>,
icon: String? = "lock"
) -> SimplePickerView<WiFiInputView.WiFiEncryptionType> {
SimplePickerView<WiFiInputView.WiFiEncryptionType>(
title: "加密类型",
selection: selection,
options: WiFiInputView.WiFiEncryptionType.allCases,
optionTitle: { $0.displayName },
icon: icon
)
}
static func socialPlatform(
selection: Binding<SocialInputView.SocialPlatform>,
icon: String? = "globe"
) -> SimplePickerView<SocialInputView.SocialPlatform> {
SimplePickerView<SocialInputView.SocialPlatform>(
title: "社交平台",
selection: selection,
options: SocialInputView.SocialPlatform.allCases,
optionTitle: { $0.displayName },
icon: icon
)
}
static func phoneType(
selection: Binding<PhoneInputView.PhoneInputType>,
icon: String? = "phone"
) -> SimplePickerView<PhoneInputView.PhoneInputType> {
SimplePickerView<PhoneInputView.PhoneInputType>(
title: "电话类型",
selection: selection,
options: PhoneInputView.PhoneInputType.allCases,
optionTitle: { $0.displayName },
icon: icon
)
}
}
#Preview {
VStack(spacing: 16) {
// WiFi
SimplePickerView<WiFiInputView.WiFiEncryptionType>.wifiEncryption(
selection: .constant(.wpa2)
)
//
SimplePickerView<SocialInputView.SocialPlatform>.socialPlatform(
selection: .constant(.instagram)
)
//
SimplePickerView<PhoneInputView.PhoneInputType>.phoneType(
selection: .constant(.phone)
)
}
.padding()
}

@ -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
)
}

@ -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)
}
// (WhatsAppViber)
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
)
}

@ -0,0 +1,200 @@
import SwiftUI
// MARK: -
struct TextEditorView: View {
let title: String
let isRequired: Bool
let placeholder: String
let text: Binding<String>
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<String>,
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<String>,
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<String>,
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<String>,
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()
}

@ -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
)
}

@ -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("• 可以输入完整URLhttps://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(""))
}

@ -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[..<index]) + suffix
}
/// URL
func withURLProtocol() -> 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)
}
}

@ -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()
}

@ -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)
)
}

@ -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)
}
}
Loading…
Cancel
Save