feat: 订阅页面

This commit is contained in:
Junhui Chen 2025-08-19 20:53:57 +08:00
parent 7d40fe3203
commit 4be13a7141
2 changed files with 314 additions and 0 deletions

View File

@ -0,0 +1,314 @@
//
// CreditsInfoCard.swift
// wake
//
// Created by fairclip on 2025/8/19.
//
import SwiftUI
// MARK: -
enum CreditType: String, CaseIterable {
case daily = "Daily"
case purchased = "Purchased"
case bonus = "Bonus"
case permanent = "Permanent"
var displayName: String {
switch self {
case .daily:
return "Daily Credits"
case .purchased:
return "Purchased Credits"
case .bonus:
return "Bonus Credits"
case .permanent:
return "Permanent Credits"
}
}
var icon: String {
switch self {
case .daily:
return "calendar"
case .purchased:
return "creditcard"
case .bonus:
return "gift"
case .permanent:
return "infinity"
}
}
var color: Color {
switch self {
case .daily:
return Theme.Colors.info
case .purchased:
return Theme.Colors.success
case .bonus:
return Theme.Colors.warning
case .permanent:
return Theme.Colors.primary
}
}
}
// MARK: -
struct CreditInfo {
let type: CreditType
let amount: Int
let description: String
}
// MARK: -
struct CreditsInfoCard: View {
let totalCredits: Int
let creditBreakdown: [CreditInfo]
let onInfoTap: (() -> Void)?
let onDetailTap: (() -> Void)?
@State private var showBreakdown = false
init(
totalCredits: Int,
creditBreakdown: [CreditInfo] = [],
onInfoTap: (() -> Void)? = nil,
onDetailTap: (() -> Void)? = nil
) {
self.totalCredits = totalCredits
self.creditBreakdown = creditBreakdown
self.onInfoTap = onInfoTap
self.onDetailTap = onDetailTap
}
var body: some View {
VStack(spacing: 0) {
//
mainCreditsSection
//
if showBreakdown && !creditBreakdown.isEmpty {
creditsBreakdownSection
}
}
.background(Color(.systemBackground))
.cornerRadius(Theme.CornerRadius.medium)
.shadow(color: Theme.Shadows.small, radius: Theme.Shadows.cardShadow.radius, x: Theme.Shadows.cardShadow.x, y: Theme.Shadows.cardShadow.y)
}
// MARK: -
private var mainCreditsSection: some View {
HStack(spacing: Theme.Spacing.lg) {
//
HStack(spacing: Theme.Spacing.sm) {
Circle()
.fill(Theme.Gradients.primaryGradient)
.frame(width: 40, height: 40)
.overlay(
Image(systemName: "star.fill")
.foregroundColor(.white)
.font(.system(size: 18, weight: .semibold))
)
VStack(alignment: .leading, spacing: 2) {
Text("Credits")
.font(Typography.font(for: .caption, family: .quicksand))
.foregroundColor(Theme.Colors.textSecondary)
Text("\(totalCredits)")
.font(Typography.font(for: .title, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
}
}
Spacer()
//
HStack(spacing: Theme.Spacing.sm) {
//
Button(action: {
onInfoTap?()
}) {
Image(systemName: "info.circle")
.foregroundColor(Theme.Colors.textSecondary)
.font(.system(size: 16))
}
// /
if !creditBreakdown.isEmpty {
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
showBreakdown.toggle()
}
}) {
Image(systemName: showBreakdown ? "chevron.up" : "chevron.down")
.foregroundColor(Theme.Colors.textSecondary)
.font(.system(size: 14, weight: .medium))
}
}
//
Button(action: {
onDetailTap?()
}) {
Image(systemName: "chevron.right")
.foregroundColor(Theme.Colors.textSecondary)
.font(.system(size: 14, weight: .medium))
}
}
}
.padding(Theme.Spacing.lg)
}
// MARK: -
private var creditsBreakdownSection: some View {
VStack(spacing: 0) {
Divider()
.background(Theme.Colors.border)
VStack(spacing: Theme.Spacing.sm) {
ForEach(Array(creditBreakdown.enumerated()), id: \.offset) { index, credit in
CreditBreakdownRow(credit: credit, isLast: index == creditBreakdown.count - 1)
}
}
.padding(Theme.Spacing.lg)
}
}
}
// MARK: -
struct CreditBreakdownRow: View {
let credit: CreditInfo
let isLast: Bool
var body: some View {
HStack(spacing: Theme.Spacing.md) {
//
Circle()
.fill(credit.type.color.opacity(0.1))
.frame(width: 32, height: 32)
.overlay(
Image(systemName: credit.type.icon)
.foregroundColor(credit.type.color)
.font(.system(size: 14, weight: .medium))
)
//
VStack(alignment: .leading, spacing: 2) {
Text(credit.type.displayName)
.font(Typography.font(for: .subtitle, family: .quicksand))
.foregroundColor(Theme.Colors.textPrimary)
Text(credit.description)
.font(Typography.font(for: .caption, family: .quicksand))
.foregroundColor(Theme.Colors.textSecondary)
.lineLimit(2)
}
Spacer()
//
Text("+\(credit.amount)")
.font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(credit.type.color)
}
.padding(.vertical, Theme.Spacing.xs)
if !isLast {
Divider()
.background(Theme.Colors.borderLight)
.padding(.leading, 44)
}
}
}
// MARK: - 使
struct CreditsUsageCard: View {
let todayUsed: Int
let weeklyUsed: Int
let monthlyUsed: Int
var body: some View {
VStack(spacing: Theme.Spacing.md) {
HStack {
Text("Credits Usage")
.font(Typography.font(for: .subtitle, family: .quicksandBold))
.foregroundColor(Theme.Colors.textPrimary)
Spacer()
Text("This Period")
.font(Typography.font(for: .caption, family: .quicksand))
.foregroundColor(Theme.Colors.textSecondary)
}
HStack(spacing: Theme.Spacing.lg) {
UsageStatItem(title: "Today", value: todayUsed, color: Theme.Colors.info)
Divider()
.frame(height: 40)
UsageStatItem(title: "Week", value: weeklyUsed, color: Theme.Colors.warning)
Divider()
.frame(height: 40)
UsageStatItem(title: "Month", value: monthlyUsed, color: Theme.Colors.success)
}
}
.padding(Theme.Spacing.lg)
.background(Color(.systemBackground))
.cornerRadius(Theme.CornerRadius.medium)
.shadow(color: Theme.Shadows.small, radius: Theme.Shadows.cardShadow.radius, x: Theme.Shadows.cardShadow.x, y: Theme.Shadows.cardShadow.y)
}
}
// MARK: - 使
struct UsageStatItem: View {
let title: String
let value: Int
let color: Color
var body: some View {
VStack(spacing: Theme.Spacing.xs) {
Text("\(value)")
.font(Typography.font(for: .title, family: .quicksandBold))
.foregroundColor(color)
Text(title)
.font(Typography.font(for: .caption, family: .quicksand))
.foregroundColor(Theme.Colors.textSecondary)
}
.frame(maxWidth: .infinity)
}
}
// MARK: -
#Preview("Credits Info Card") {
VStack(spacing: 20) {
CreditsInfoCard(
totalCredits: 3290,
creditBreakdown: [
CreditInfo(type: .daily, amount: 200, description: "Daily free credits"),
CreditInfo(type: .purchased, amount: 1000, description: "Purchased package"),
CreditInfo(type: .bonus, amount: 500, description: "Welcome bonus"),
CreditInfo(type: .permanent, amount: 1590, description: "Subscription credits")
],
onInfoTap: {
print("Info tapped")
},
onDetailTap: {
print("Detail tapped")
}
)
CreditsUsageCard(
todayUsed: 45,
weeklyUsed: 280,
monthlyUsed: 1150
)
}
.padding()
.background(Color(.systemGroupedBackground))
}