237 lines
7.6 KiB
Swift
237 lines
7.6 KiB
Swift
//
|
||
// SubscriptionStatusBar.swift
|
||
// wake
|
||
//
|
||
// Created by fairclip on 2025/8/19.
|
||
//
|
||
|
||
import SwiftUI
|
||
|
||
// MARK: - 订阅状态枚举
|
||
enum SubscriptionStatus {
|
||
case free
|
||
case pioneer(expiryDate: Date)
|
||
|
||
var title: String {
|
||
switch self {
|
||
case .free:
|
||
return "Free"
|
||
case .pioneer:
|
||
return "Pioneer"
|
||
}
|
||
}
|
||
|
||
var backgroundColor: Color {
|
||
switch self {
|
||
case .free:
|
||
return .clear
|
||
case .pioneer:
|
||
return .clear
|
||
}
|
||
}
|
||
|
||
var textColor: Color {
|
||
switch self {
|
||
case .free:
|
||
return Theme.Colors.textPrimary
|
||
case .pioneer:
|
||
return .themeTextMessageMain
|
||
}
|
||
}
|
||
|
||
var backgroundImageName: String {
|
||
switch self {
|
||
case .free:
|
||
return "Free"
|
||
case .pioneer:
|
||
return "Pioneer"
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 订阅状态栏组件
|
||
struct SubscriptionStatusBar: View {
|
||
let status: SubscriptionStatus
|
||
let onSubscribeTap: (() -> Void)?
|
||
let size: String
|
||
private let height: CGFloat
|
||
private let backgroundColor: Color?
|
||
|
||
init(status: SubscriptionStatus, height: CGFloat? = nil, backgroundColor: Color? = nil, onSubscribeTap: (() -> Void)? = nil, size: String? = "md") {
|
||
self.status = status
|
||
self.height = height ?? 155 // 默认高度为155
|
||
self.backgroundColor = backgroundColor
|
||
self.onSubscribeTap = onSubscribeTap
|
||
self.size = size ?? "md"
|
||
}
|
||
|
||
var body: some View {
|
||
ZStack(alignment: .leading) {
|
||
// SwiftUI 绘制的背景
|
||
SubscriptionBackground(status: status, customBackground: backgroundColor, size: size)
|
||
.frame(maxWidth: .infinity, minHeight: 120)
|
||
.clipped()
|
||
|
||
// Main content container
|
||
VStack(alignment: .leading, spacing: 0) {
|
||
// Title - Centered vertically
|
||
Text(status.title)
|
||
.font(.system(size: size == "sm" ? 24 : 28, weight: .bold, design: .rounded))
|
||
.foregroundColor(status.textColor)
|
||
.frame(maxHeight: .infinity, alignment: .center) // Center vertically
|
||
.padding(.leading, 12)
|
||
.padding(.top, height < 155 ? 30 : 40)
|
||
|
||
// Expiry date - Bottom left
|
||
if case .pioneer(let expiryDate) = status {
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
Text("Expires on :")
|
||
.font(.system(size: 12))
|
||
.foregroundColor(.themeTextMessageMain)
|
||
|
||
Text(formatDate(expiryDate))
|
||
.font(.system(size: 12))
|
||
.fontWeight(.bold)
|
||
.foregroundColor(.themeTextMessageMain)
|
||
}
|
||
.padding(.leading, 12)
|
||
.padding(.bottom, 12)
|
||
}
|
||
}
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||
}
|
||
.frame(height: height)
|
||
}
|
||
|
||
// MARK: - 日期格式化
|
||
private func formatDate(_ date: Date) -> String {
|
||
let formatter = DateFormatter()
|
||
formatter.dateFormat = "MMM d, yyyy"
|
||
return formatter.string(from: date)
|
||
}
|
||
}
|
||
|
||
// MARK: - 背景绘制
|
||
private struct SubscriptionBackground: View {
|
||
let status: SubscriptionStatus
|
||
let customBackground: Color?
|
||
let size: String
|
||
|
||
private let parallelogramHeight: CGFloat
|
||
private let parallelogramWidth: CGFloat
|
||
|
||
init(status: SubscriptionStatus, customBackground: Color? = nil, size: String? = "md") {
|
||
self.status = status
|
||
self.customBackground = customBackground
|
||
self.size = size ?? "md"
|
||
self.parallelogramHeight = size == "sm" ? 14 : 18
|
||
self.parallelogramWidth = size == "sm" ? 12 : 16
|
||
}
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
// 背景底板:自定义颜色优先,否则根据状态给出渐变
|
||
if let color = customBackground {
|
||
RoundedRectangle(cornerRadius: 10)
|
||
.fill(color)
|
||
.shadow(color: Color.black.opacity(0.06), radius: 10, x: 0, y: 6)
|
||
} else {
|
||
RoundedRectangle(cornerRadius: 10)
|
||
.fill(defaultBackground)
|
||
.shadow(color: Color.black.opacity(0.06), radius: 10, x: 0, y: 6)
|
||
}
|
||
|
||
// 左上角斜条纹(装饰)
|
||
VStack {
|
||
HStack {
|
||
ParallelogramRow(
|
||
count: 6,
|
||
itemSize: CGSize(width: parallelogramWidth, height: parallelogramHeight),
|
||
shear: -0.35,
|
||
cornerRadius: 2,
|
||
color: .black.opacity(0.85),
|
||
spacing: 1
|
||
)
|
||
Spacer()
|
||
}
|
||
Spacer()
|
||
}
|
||
.padding(.top, 12)
|
||
.padding(.leading, 16)
|
||
|
||
// 右下角斜条纹(装饰)
|
||
VStack {
|
||
Spacer()
|
||
HStack {
|
||
Spacer()
|
||
ParallelogramRow(
|
||
count: 3,
|
||
itemSize: CGSize(width: parallelogramWidth, height: parallelogramHeight),
|
||
shear: -0.35,
|
||
cornerRadius: 2,
|
||
color: .black.opacity(0.85),
|
||
spacing: 1
|
||
)
|
||
}
|
||
}
|
||
.padding(.bottom, 14)
|
||
.padding(.trailing, 16)
|
||
|
||
// 右上角圆形徽标 + 三角指针(与示例类似)
|
||
VStack {
|
||
HStack {
|
||
Spacer()
|
||
ZStack {
|
||
CircleView(diameter: size == "sm" ? 70 : 100, color: .black)
|
||
TriangleView(
|
||
width: size == "sm" ? 20 : 24,
|
||
height: size == "sm" ? 20 : 24,
|
||
direction: .right,
|
||
color: Color(white: 0.9),
|
||
rotation: .degrees(157)
|
||
)
|
||
}
|
||
.offset(x: size == "sm" ? 8 : 12, y: size == "sm" ? -8 : -12)
|
||
}
|
||
Spacer()
|
||
}
|
||
}
|
||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||
}
|
||
|
||
private var defaultBackground: LinearGradient {
|
||
switch status {
|
||
case .free:
|
||
return LinearGradient(colors: [Color(hex: "FFF8DE")], startPoint: .topLeading, endPoint: .bottomTrailing)
|
||
case .pioneer:
|
||
return LinearGradient(colors: [Color.themePrimary.opacity(0.85), Color.orange.opacity(0.6)], startPoint: .topLeading, endPoint: .bottomTrailing)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 预览
|
||
#Preview {
|
||
VStack(spacing: 20) {
|
||
// Free status preview
|
||
SubscriptionStatusBar(
|
||
status: .free,
|
||
backgroundColor: Color(white: 0.98),
|
||
onSubscribeTap: {
|
||
print("Subscribe tapped")
|
||
}
|
||
)
|
||
.padding(.horizontal)
|
||
|
||
// Pioneer status preview
|
||
SubscriptionStatusBar(
|
||
status: .pioneer(
|
||
expiryDate: Calendar.current.date(byAdding: .month, value: 6, to: Date()) ?? Date()
|
||
),
|
||
backgroundColor: Color.orange.opacity(0.9)
|
||
)
|
||
.padding(.horizontal)
|
||
}
|
||
.padding()
|
||
.background(Theme.Colors.background)
|
||
}
|