You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

325 lines
9.0 KiB

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