diff --git a/wake/View/Credits/CreditsInfoCard.swift b/wake/View/Credits/CreditsInfoCard.swift new file mode 100644 index 0000000..1ec5010 --- /dev/null +++ b/wake/View/Credits/CreditsInfoCard.swift @@ -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)) +} diff --git a/wake/View/Subscribe/Components/PlanSelector.swift b/wake/View/Subscribe/Components/PlanSelector.swift new file mode 100644 index 0000000..e69de29