parent
d5fef06e76
commit
12764a2598
@ -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,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,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,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)
|
||||
)
|
||||
}
|
Loading…
Reference in new issue