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.
318 lines
9.2 KiB
318 lines
9.2 KiB
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
|
|
} |