diff --git a/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate b/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate index 6e6e724..8717dea 100644 Binary files a/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate and b/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/wake.xcodeproj/xcuserdata/fairclip.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/wake.xcodeproj/xcuserdata/fairclip.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..0ff5029 --- /dev/null +++ b/wake.xcodeproj/xcuserdata/fairclip.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/wake/Theme.swift b/wake/Theme.swift index 7a3f307..dfcc994 100644 --- a/wake/Theme.swift +++ b/wake/Theme.swift @@ -23,7 +23,7 @@ struct Theme { static let accent = Color(hex: "FF6B6B") // 强调红色 // MARK: - 中性色 - static let background = Color(hex: "F8F9FA") // 背景色 + static let background = Color(hex: "FAFAFA") // 背景色 static let surface = Color.white // 表面色 static let surfaceSecondary = Color(hex: "F5F5F5") // 次级表面色 @@ -40,14 +40,18 @@ struct Theme { static let info = Color(hex: "3B82F6") // 信息色 // MARK: - 边框色 - static let border = Color(hex: "E5E7EB") // 边框色 + static let border = Color(hex: "D9D9D9") // 边框色 static let borderLight = Color(hex: "F3F4F6") // 浅边框色 - static let borderDark = Color(hex: "D1D5DB") // 深边框色 + static let borderBlack = Color.black // 黑色边框色 + static let borderDark = borderBlack // 深边框色 // MARK: - 订阅相关色 static let freeBackground = primaryLight // Free版背景 static let pioneerBackground = primary // Pioneer版背景 static let subscribeButton = primary // 订阅按钮色 + + // MARK: - 卡片相关色 + static let cardBackground = Color.white // 卡片背景 } // MARK: - 渐变色 @@ -59,9 +63,13 @@ struct Theme { ) static let backgroundGradient = LinearGradient( - colors: [Colors.background, Colors.surface], - startPoint: .top, - endPoint: .bottom + gradient: Gradient(colors: [ + Color(hex: "FBC063"), + Color(hex: "FEE9BE"), + Color(hex: "FAB851") + ]), + startPoint: .topLeading, + endPoint: .bottomTrailing ) static let accentGradient = LinearGradient( @@ -69,6 +77,12 @@ struct Theme { startPoint: .leading, endPoint: .trailing ) + + // static let creditsInfoTooltip = LinearGradient( + // colors: [Colors(hex: "FFD38F"), Colors(hex: "FFF8DE"), Colors(hex: "FECE83")], + // startPoint: .topLeading, + // endPoint: .bottomTrailing + // ) } // MARK: - 阴影 diff --git a/wake/Typography.swift b/wake/Typography.swift index 27f7b98..91ba2d3 100644 --- a/wake/Typography.swift +++ b/wake/Typography.swift @@ -60,21 +60,21 @@ struct Typography { /// - style: 文本样式 /// - family: 字体库,默认为 nil 使用默认字体库 /// - Returns: 配置好的 Font 对象 - static func font(for style: TypographyStyle, family: FontFamily? = nil) -> Font { + static func font(for style: TypographyStyle, family: FontFamily? = nil, size: CGFloat? = nil) -> Font { let fontFamily = family ?? defaultFontFamily guard let config = styleConfig[style] else { return .body } // 尝试加载自定义字体 - if let customFont = UIFont(name: fontFamily.name, size: config.size) { + if let customFont = UIFont(name: fontFamily.name, size: size ?? config.size) { let metrics = UIFontMetrics(forTextStyle: config.textStyle) let scaledFont = metrics.scaledFont(for: customFont) return Font(scaledFont) } // 如果自定义字体加载失败,回退到系统字体 - let systemFont = UIFont.systemFont(ofSize: config.size, weight: config.weight) + let systemFont = UIFont.systemFont(ofSize: size ?? config.size, weight: config.weight) let metrics = UIFontMetrics(forTextStyle: config.textStyle) let scaledFont = metrics.scaledFont(for: systemFont) return Font(scaledFont) diff --git a/wake/View/Credits/CreditsDetailView.swift b/wake/View/Credits/CreditsDetailView.swift new file mode 100644 index 0000000..8d6c496 --- /dev/null +++ b/wake/View/Credits/CreditsDetailView.swift @@ -0,0 +1,288 @@ +// +// CreditsDetailView.swift +// wake +// +// Created by fairclip on 2025/8/19. +// + +import SwiftUI + +// MARK: - 积分交易类型 +enum CreditTransactionType: String, CaseIterable { + case photoUnderstanding = "Photo Understanding" + case videoUnderstanding = "Video Understanding" + case mysteryBoxPurchase = "Mystery Box Purchase" + case dailyBonus = "Daily Bonus" + case subscriptionBonus = "Subscription Bonus" + + var creditChange: Int { + switch self { + case .photoUnderstanding: + return -1 + case .videoUnderstanding: + return -32 + case .mysteryBoxPurchase: + return -100 + case .dailyBonus: + return 200 + case .subscriptionBonus: + return 500 + } + } + + var icon: String { + switch self { + case .photoUnderstanding: + return "photo" + case .videoUnderstanding: + return "video" + case .mysteryBoxPurchase: + return "gift" + case .dailyBonus: + return "calendar" + case .subscriptionBonus: + return "star.fill" + } + } +} + +// MARK: - 积分交易记录 +struct CreditTransaction { + let id = UUID() + let type: CreditTransactionType + let date: Date + let creditChange: Int + + init(type: CreditTransactionType, date: Date, creditChange: Int? = nil) { + self.type = type + self.date = date + self.creditChange = creditChange ?? type.creditChange + } +} + +// MARK: - 积分详情页面 +struct CreditsDetailView: View { + @Environment(\.presentationMode) var presentationMode + @State private var showRules = false + + // 示例数据 + private let totalCredits = 3290 + private let expiringToday = 200 + private let transactions: [CreditTransaction] = [ + CreditTransaction(type: .photoUnderstanding, date: Calendar.current.date(byAdding: .hour, value: -2, to: Date()) ?? Date()), + CreditTransaction(type: .videoUnderstanding, date: Calendar.current.date(byAdding: .hour, value: -4, to: Date()) ?? Date()), + CreditTransaction(type: .mysteryBoxPurchase, date: Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date()), + CreditTransaction(type: .dailyBonus, date: Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date()), + CreditTransaction(type: .subscriptionBonus, date: Calendar.current.date(byAdding: .day, value: -2, to: Date()) ?? Date()) + ] + + var body: some View { + NavigationView { + ScrollView { + VStack(spacing: 0) { + // 导航栏 + navigationHeader + + // 主积分卡片 + mainCreditsCard + + // 积分历史 + creditsHistorySection + + Spacer(minLength: 100) + } + } + .background(Color(.systemGroupedBackground)) + .navigationBarHidden(true) + } + } + + // MARK: - 导航栏 + private var navigationHeader: some View { + NaviHeader(title: "Credits") { + presentationMode.wrappedValue.dismiss() + } + } + + // MARK: - 主积分卡片 + private var mainCreditsCard: some View { + VStack(spacing: 0) { + // 主要积分显示区域 + HStack { + // 左侧三角形图标 + Circle() + .fill(Color.black) + .frame(width: 80, height: 80) + .overlay( + Image(systemName: "triangle.fill") + .foregroundColor(.white) + .font(.system(size: 24, weight: .bold)) + ) + + Spacer() + + // 右侧积分信息 + VStack(alignment: .trailing, spacing: 8) { + HStack(spacing: 8) { + Circle() + .fill(Color.black) + .frame(width: 24, height: 24) + .overlay( + Image(systemName: "triangle.fill") + .foregroundColor(.white) + .font(.system(size: 8)) + ) + + Text("\(totalCredits)") + .font(Typography.font(for: .headline, family: .quicksandBold, size: 36)) + .foregroundColor(.black) + } + + Text("Expiring Today : \(expiringToday)") + .font(Typography.font(for: .body, family: .quicksand)) + .foregroundColor(.black.opacity(0.8)) + } + } + .padding(Theme.Spacing.xl) + + // 虚线分隔 + DashedLine() + .stroke(Color.black.opacity(0.3), style: StrokeStyle(lineWidth: 1, dash: [5, 5])) + .frame(height: 1) + .padding(.horizontal, Theme.Spacing.xl) + + // 积分规则展开区域 + creditsRulesSection + } + .background( + LinearGradient( + colors: [ + Color(hex: "FFB645"), + Color(hex: "FFA726") + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .cornerRadius(Theme.CornerRadius.large) + .padding(.horizontal, Theme.Spacing.xl) + .padding(.top, Theme.Spacing.xl) + } + + // MARK: - 积分规则区域 + private var creditsRulesSection: some View { + VStack(spacing: 0) { + // 规则标题按钮 + Button(action: { + withAnimation(.easeInOut(duration: 0.3)) { + showRules.toggle() + } + }) { + HStack { + Text("Credits Rules") + .font(Typography.font(for: .body, family: .quicksandBold)) + .foregroundColor(.black) + + Spacer() + + Image(systemName: showRules ? "chevron.up" : "chevron.down") + .foregroundColor(.black) + .font(.system(size: 14, weight: .medium)) + } + .padding(.horizontal, Theme.Spacing.xl) + .padding(.vertical, Theme.Spacing.lg) + } + + // 规则内容 + if showRules { + VStack(alignment: .leading, spacing: Theme.Spacing.sm) { + Text("Credits can be used for material indexing (1 credit per photo or per second of video) and for buying blind boxes (100 credits each).") + .font(Typography.font(for: .subtitle, family: .quicksand)) + .foregroundColor(.black.opacity(0.8)) + .multilineTextAlignment(.leading) + } + .padding(.horizontal, Theme.Spacing.xl) + .padding(.bottom, Theme.Spacing.lg) + } + } + } + + // MARK: - 积分历史区域 + private var creditsHistorySection: some View { + VStack(alignment: .leading, spacing: Theme.Spacing.lg) { + Text("Points History") + .font(Typography.font(for: .title, family: .quicksandBold)) + .foregroundColor(Theme.Colors.textPrimary) + .padding(.horizontal, Theme.Spacing.xl) + + LazyVStack(spacing: 0) { + ForEach(Array(transactions.enumerated()), id: \.element.id) { index, transaction in + CreditTransactionRow( + transaction: transaction, + isLast: index == transactions.count - 1 + ) + } + } + .background(Color(.systemBackground)) + .cornerRadius(Theme.CornerRadius.medium) + .padding(.horizontal, Theme.Spacing.xl) + } + .padding(.top, Theme.Spacing.xl) + } +} + +// MARK: - 积分交易行组件 +struct CreditTransactionRow: View { + let transaction: CreditTransaction + let isLast: Bool + + var body: some View { + VStack(spacing: 0) { + HStack(spacing: Theme.Spacing.lg) { + VStack(alignment: .leading, spacing: 4) { + Text(transaction.type.rawValue) + .font(Typography.font(for: .body, family: .quicksandBold)) + .foregroundColor(Theme.Colors.textPrimary) + + Text(formatDate(transaction.date)) + .font(Typography.font(for: .caption, family: .quicksand)) + .foregroundColor(Theme.Colors.textSecondary) + } + + Spacer() + + Text("\(transaction.creditChange > 0 ? "+" : "")\(transaction.creditChange)") + .font(Typography.font(for: .body, family: .quicksandBold)) + .foregroundColor(transaction.creditChange > 0 ? Theme.Colors.success : Theme.Colors.textPrimary) + } + .padding(.horizontal, Theme.Spacing.lg) + .padding(.vertical, Theme.Spacing.lg) + + if !isLast { + Divider() + .background(Theme.Colors.borderLight) + } + } + } + + private func formatDate(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "MM-dd-yyyy" + return formatter.string(from: date) + } +} + +// MARK: - 虚线组件 +struct DashedLine: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + path.move(to: CGPoint(x: 0, y: 0)) + path.addLine(to: CGPoint(x: rect.width, y: 0)) + return path + } +} + +// MARK: - 预览 +#Preview { + CreditsDetailView() +} diff --git a/wake/View/Credits/CreditsInfoCard.swift b/wake/View/Credits/CreditsInfoCard.swift new file mode 100644 index 0000000..b374a4c --- /dev/null +++ b/wake/View/Credits/CreditsInfoCard.swift @@ -0,0 +1,107 @@ + // +// CreditsInfoCard.swift +// wake +// +// Created by fairclip on 2025/8/19. +// + +import SwiftUI + +// MARK: - 积分信息卡片组件 +struct CreditsInfoCard: View { + let totalCredits: Int + let onInfoTap: (() -> Void)? + let onDetailTap: (() -> Void)? + + @State private var showInfoPopover = false + + init( + totalCredits: Int, + onInfoTap: (() -> Void)? = nil, + onDetailTap: (() -> Void)? = nil + ) { + self.totalCredits = totalCredits + self.onInfoTap = onInfoTap + self.onDetailTap = onDetailTap + } + + var body: some View { + Button(action: { + onDetailTap?() + }) { + mainCreditsSection + } + .buttonStyle(PlainButtonStyle()) + .background(Theme.Colors.primaryLight) + .cornerRadius(Theme.CornerRadius.extraLarge) + .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.md) { + // 积分图标和数量 + HStack(spacing: Theme.Spacing.sm) { + Text("Credits:") + .font(Typography.font(for: .subtitle, family: .quicksandBold)) + .foregroundColor(Theme.Colors.textPrimary) + + Text("\(totalCredits)") + .font(Typography.font(for: .subtitle, family: .quicksandBold)) + .foregroundColor(Theme.Colors.textPrimary) + } + + + // 操作按钮区域 + HStack(spacing: Theme.Spacing.sm) { + // 信息按钮 + Button(action: { + showInfoPopover = true + onInfoTap?() + }) { + Image(systemName: "questionmark.circle") + .foregroundColor(Theme.Colors.textSecondary) + .font(.system(size: 16)) + } + .popover(isPresented: $showInfoPopover, attachmentAnchor: .point(.bottom), arrowEdge: .top) { + Text("Credits can be used for material indexing (1 credit per photo or per second of video) and for buying blind boxes (100 crediteach)") + .font(Typography.font(for: .caption, family: .quicksandRegular)) + .multilineTextAlignment(.center) + .presentationBackground(Theme.Gradients.backgroundGradient) + .frame(minWidth: 240, maxWidth: UIScreen.main.bounds.width * 0.6) + .presentationCompactAdaptation(.popover) + .padding(.horizontal, Theme.Spacing.md) + .padding(.vertical, Theme.Spacing.sm) + } + + Spacer() + + // 详情按钮 + Image(systemName: "chevron.right") + .foregroundColor(Theme.Colors.textPrimary) + .font(.system(size: 14, weight: .medium)) + } + } + .padding(Theme.Spacing.lg) + } + + +} + + +// MARK: - 预览 +#Preview("Credits Info Card") { + VStack(spacing: 20) { + CreditsInfoCard( + totalCredits: 3290, + onInfoTap: { + print("Info tapped") + }, + onDetailTap: { + print("Detail tapped") + } + ) + } + .padding() + .background(Color(.systemGroupedBackground)) +} diff --git a/wake/View/Subscribe/Components/PlanCompare.swift b/wake/View/Subscribe/Components/PlanCompare.swift new file mode 100644 index 0000000..77fc88c --- /dev/null +++ b/wake/View/Subscribe/Components/PlanCompare.swift @@ -0,0 +1,165 @@ +// +// PlanCompare.swift +// wake +// +// Created by fairclip on 2025/8/20. +// + +import SwiftUI + +// MARK: - 计划对比功能数据模型 +struct PlanFeature { + let title: String + let subtitle: String? + let freeValue: String + let pioneerValue: String + let icon: String? +} + +// MARK: - 计划对比组件 +struct PlanCompare: View { + + // MARK: - 功能对比数据 + private let features: [PlanFeature] = [ + PlanFeature( + title: "Mystery Box Purchase:", + subtitle: nil, + freeValue: "3 /week", + pioneerValue: "Free", + icon: nil + ), + PlanFeature( + title: "Material Upload:", + subtitle: nil, + freeValue: "50 images and\n5 videos/day", + pioneerValue: "Unlimited", + icon: nil + ), + PlanFeature( + title: "Free Credits:", + subtitle: "Expires the next day", + freeValue: "200 /day", + pioneerValue: "500 /day", + icon: nil + ) + ] + + var body: some View { + HStack(spacing: 0) { + // 功能名称列 + featureNamesColumn + .frame(minWidth: 163) + + // Free 计划列(更宽,优先占据剩余空间) + planColumn(title: "Free", isPioneer: false) + .layoutPriority(1) + + // Pioneer 计划列(固定较窄宽度) + planColumn(title: "Pioneer", isPioneer: true) + .frame(width: 88) + } + .background(Theme.Colors.cardBackground) + .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 featureNamesColumn: some View { + VStack(spacing: 0) { + // 表头 + Text("") + .font(Typography.font(for: .title, family: .quicksandBold, size: 14)) + .padding(.vertical, Theme.Spacing.sm) + .frame(maxWidth: .infinity, minHeight: 30) + + // 功能名称 + ForEach(Array(features.enumerated()), id: \.offset) { index, feature in + VStack(alignment: .leading, spacing: Theme.Spacing.xs) { + Text(feature.title) + .font(Typography.font(for: .body, family: .quicksandBold, size: 12)) + .foregroundColor(Theme.Colors.textPrimary) + .multilineTextAlignment(.leading) + + if let subtitle = feature.subtitle { + Text(subtitle) + .font(Typography.font(for: .caption, family: .quicksandRegular)) + .foregroundColor(Theme.Colors.textSecondary) + .multilineTextAlignment(.leading) + } + } + .frame(maxWidth: .infinity, minHeight: 30, alignment: .leading) + .padding(.horizontal, Theme.Spacing.sm) + .padding(.vertical, Theme.Spacing.sm) + } + } + .padding(Theme.Spacing.sm) + } + + // MARK: - 计划列 + private func planColumn(title: String, isPioneer: Bool) -> some View { + VStack(spacing: 0) { + // 表头 + VStack(spacing: Theme.Spacing.xs) { + Text(title) + .font(Typography.font(for: .title, family: .quicksandBold, size: 14)) + .foregroundColor(Color.black) + } + .frame(maxWidth: .infinity) + .padding(.vertical, Theme.Spacing.sm) + + // 功能值 + ForEach(Array(features.enumerated()), id: \.offset) { index, feature in + let value = isPioneer ? feature.pioneerValue : feature.freeValue + + Text(value) + .font(Typography.font(for: .body, family: .quicksandRegular, size: 12)) + .foregroundColor(isPioneer ? Color.black : Theme.Colors.textSecondary) + .fontWeight(isPioneer ? .semibold : .regular) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, minHeight: 30) + .padding(.vertical, Theme.Spacing.sm) + + } + } + .frame(maxWidth: .infinity) + .background(isPioneer ? Theme.Colors.primaryLight : Color.white) + .cornerRadius(Theme.CornerRadius.medium) + .overlay( + RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) + .stroke( + isPioneer ? Theme.Colors.primary : Theme.Colors.border, + lineWidth: isPioneer ? 1 : 0 + ) + ) + .padding(Theme.Spacing.sm) + + } +} + + +// MARK: - 预览 +#Preview("PlanCompare") { + ScrollView { + VStack(spacing: Theme.Spacing.xl) { + PlanCompare() + } + .padding() + } + .background(Theme.Colors.background) +} + +#Preview("PlanCompare Dark") { + ScrollView { + VStack(spacing: Theme.Spacing.xl) { + PlanCompare() + } + .padding() + } + .background(Color.black) + .preferredColorScheme(.dark) +} \ No newline at end of file diff --git a/wake/View/Subscribe/Components/PlanSelector.swift b/wake/View/Subscribe/Components/PlanSelector.swift new file mode 100644 index 0000000..d37fe54 --- /dev/null +++ b/wake/View/Subscribe/Components/PlanSelector.swift @@ -0,0 +1,135 @@ +// +// PlanSelector.swift +// wake +// +// Created by fairclip on 2025/8/19. +// + +import SwiftUI + +// MARK: - 计划选择器组件 +struct PlanSelector: View { + @Binding var selectedPlan: SubscriptionPlan? + let onPlanSelected: (SubscriptionPlan) -> Void + + private let plans: [SubscriptionPlan] = [.free, .pioneer] + + init( + selectedPlan: Binding, + onPlanSelected: @escaping (SubscriptionPlan) -> Void = { _ in } + ) { + self._selectedPlan = selectedPlan + self.onPlanSelected = onPlanSelected + } + + var body: some View { + HStack(spacing: Theme.Spacing.md) { + ForEach(plans, id: \.self) { plan in + PlanCard( + plan: plan, + isSelected: selectedPlan == plan, + onTap: { + selectedPlan = plan + onPlanSelected(plan) + } + ) + } + } + } +} + +// MARK: - 单个计划卡片 +struct PlanCard: View { + let plan: SubscriptionPlan + let isSelected: Bool + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + ZStack { + // 主卡片内容 + VStack(spacing: Theme.Spacing.sm) { + // Popular 标签 + if plan.isPopular { + VStack { + HStack { + Spacer() + Text("Popular") + .font(Typography.font(for: .caption, family: .quicksandRegular)) + .foregroundColor(Color.white) + .padding(.horizontal, Theme.Spacing.sm) + .padding(.vertical, Theme.Spacing.xs) + .background(Color.black) + .cornerRadius(Theme.CornerRadius.round, corners: [.bottomLeft]) + } + Spacer() + + VStack { + // 计划名称 + Text(plan.displayName) + .font(Typography.font(for: .title, family: .quicksandBold, size: 18)) + .foregroundColor(plan == .pioneer ? Theme.Colors.textPrimary: Theme.Colors.textTertiary ) + + // 价格 + if plan == .pioneer { + Text(plan.price) + .font(Typography.font(for: .body, family: .quicksandBold, size: 20)) + .foregroundColor(Theme.Colors.textPrimary) + } + } + Spacer() + Spacer() + } + } + else { + // 计划名称 + Text(plan.displayName) + .font(Typography.font(for: .title, family: .quicksandBold, size: 18)) + .foregroundColor(plan == .pioneer ? Theme.Colors.textPrimary: Theme.Colors.textTertiary ) + + // 价格 + if plan == .pioneer { + Text(plan.price) + .font(Typography.font(for: .body, family: .quicksandBold, size: 20)) + .foregroundColor(Theme.Colors.textPrimary) + } + } + } + .frame(maxWidth: .infinity) + .frame(height: 120) + .background( + plan == .pioneer ? + Theme.Colors.primary : + Theme.Colors.surface + ) + .overlay( + RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) + .stroke( + isSelected ? Theme.Colors.borderDark : Theme.Colors.border, + lineWidth: 2 + ) + ) + .cornerRadius(Theme.CornerRadius.medium) + } + } + .buttonStyle(PlainButtonStyle()) + } +} + + +// MARK: - 预览 +#Preview("Plan Selector") { + @State var selectedPlan: SubscriptionPlan? = .pioneer + + VStack(spacing: 20) { + PlanSelector( + selectedPlan: $selectedPlan, + onPlanSelected: { plan in + print("Selected plan: \(plan.displayName)") + } + ) + + } + .padding() + .background(Color(.systemGroupedBackground)) +} diff --git a/wake/View/Subscribe/Components/SubscribeButton.swift b/wake/View/Subscribe/Components/SubscribeButton.swift new file mode 100644 index 0000000..307cdad --- /dev/null +++ b/wake/View/Subscribe/Components/SubscribeButton.swift @@ -0,0 +1,69 @@ +import SwiftUI + +// MARK: - Subscribe Button +struct SubscribeButton: View { + let title: String + let isLoading: Bool + let action: () -> Void + + init( + title: String = "Subscribe", + isLoading: Bool, + action: @escaping () -> Void + ) { + self.title = title + self.isLoading = isLoading + self.action = action + } + + var body: some View { + VStack(spacing: Theme.Spacing.xs) { + Button(action: { + guard !isLoading else { return } + action() + }) { + HStack(spacing: Theme.Spacing.sm) { + if isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: Theme.Colors.textInverse)) + } + + VStack { + Spacer() + Spacer() + Text(title) + .font(Typography.font(for: .body, family: .quicksandBold)) + Spacer() + // Fixed subtitle text as requested + Text("And get 5,000 Permanent Credits") + .font(Typography.font(for: .caption, family: .quicksandRegular)) + .foregroundColor(Theme.Colors.textPrimary) + Spacer() + Spacer() + } + } + .frame(height: 56) + .frame(maxWidth: .infinity) + .background(Theme.Colors.primary) // primary color background + .clipShape(Capsule()) + .shadow( + color: Theme.Shadows.buttonShadow.color, + radius: Theme.Shadows.buttonShadow.radius, + x: Theme.Shadows.buttonShadow.x, + y: Theme.Shadows.buttonShadow.y + ) + } + .buttonStyle(.plain) + .disabled(isLoading) + } + } +} + +#Preview("SubscribeButton") { + VStack(spacing: Theme.Spacing.xl) { + SubscribeButton(isLoading: false) {} + SubscribeButton(isLoading: true) {} + } + .padding() + .background(Theme.Colors.background) +} \ No newline at end of file diff --git a/wake/View/Subscribe/Components/SubscribePolicy.swift b/wake/View/Subscribe/Components/SubscribePolicy.swift new file mode 100644 index 0000000..e69de29 diff --git a/wake/View/Subscribe/Components/SubscriptionStatusBar.swift b/wake/View/Subscribe/Components/SubscriptionStatusBar.swift index 1a92267..48231b7 100644 --- a/wake/View/Subscribe/Components/SubscriptionStatusBar.swift +++ b/wake/View/Subscribe/Components/SubscriptionStatusBar.swift @@ -61,21 +61,21 @@ struct SubscriptionStatusBar: View { var body: some View { HStack(spacing: 16) { - VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: 20) { // 订阅类型标题 Text(status.title) - .font(.system(size: 28, weight: .bold, design: .rounded)) + .font(Typography.font(for: .headline, family: .quicksandBold, size: 32)) .foregroundColor(status.textColor) // 过期时间或订阅按钮 if case .pioneer(let expiryDate) = status { VStack(alignment: .leading, spacing: 4) { Text("Expires on :") - .font(.system(size: 14, weight: .medium)) - .foregroundColor(status.textColor.opacity(0.8)) + .font(Typography.font(for: .body, family: .quicksandRegular)) + .foregroundColor(status.textColor.opacity(0.7)) Text(formatDate(expiryDate)) - .font(.system(size: 16, weight: .semibold)) + .font(Typography.font(for: .body, family: .quicksandRegular)) .foregroundColor(status.textColor) } } else { @@ -83,12 +83,12 @@ struct SubscriptionStatusBar: View { onSubscribeTap?() }) { Text("Subscribe") - .font(.system(size: 14, weight: .semibold)) + .font(Typography.font(for: .title, family: .quicksandRegular, size: 16)) .foregroundColor(Theme.Colors.textPrimary) - .padding(.horizontal, 20) - .padding(.vertical, 8) - .background(Theme.Colors.subscribeButton) - .cornerRadius(Theme.CornerRadius.large) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Theme.Gradients.backgroundGradient) + .cornerRadius(Theme.CornerRadius.extraLarge) } } } diff --git a/wake/View/Subscribe/SubscribeView.swift b/wake/View/Subscribe/SubscribeView.swift index ab3eb10..d90c682 100644 --- a/wake/View/Subscribe/SubscribeView.swift +++ b/wake/View/Subscribe/SubscribeView.swift @@ -38,7 +38,7 @@ struct SubscriptionFeature { } struct SubscribeView: View { - @State private var selectedPlan: SubscriptionPlan = .free + @State private var selectedPlan: SubscriptionPlan? = .pioneer @State private var isLoading = false @Environment(\.presentationMode) var presentationMode @@ -57,16 +57,23 @@ struct SubscribeView: View { navigationHeader // 当前订阅状态卡片 - currentSubscriptionCard + currentSubscriptionCard // 积分信息 creditsSection - // 订阅计划选择 - subscriptionPlansSection - - // 特别优惠提示 - specialOfferBanner + VStack { + // 订阅计划选择 + subscriptionPlansSection + + // 特别优惠提示 + specialOfferBanner + } + .background(Theme.Colors.cardBackground) + .cornerRadius(Theme.CornerRadius.medium) + .padding(.horizontal, Theme.Spacing.lg) + .padding(.vertical, Theme.Spacing.lg) + // 功能对比表 featureComparisonTable @@ -80,7 +87,7 @@ struct SubscribeView: View { Spacer(minLength: 100) } } - .background(Color(.systemGroupedBackground)) + .background(Theme.Colors.background) .navigationBarHidden(true) } } @@ -94,197 +101,127 @@ struct SubscribeView: View { // MARK: - 当前订阅状态卡片 private var currentSubscriptionCard: some View { - VStack(spacing: 0) { - HStack { - VStack(alignment: .leading, spacing: 8) { - Text("Free") - .font(Typography.font(for: .headline, family: .quicksand)) - .fontWeight(.bold) - - Button(action: { - // 订阅操作 - }) { - Text("Subscribe") - .font(Typography.font(for: .subtitle, family: .quicksand)) - .fontWeight(.medium) - .foregroundColor(.black) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background(Color.orange) - .cornerRadius(20) - } - } - - Spacer() - - // 播放按钮图标 - Circle() - .fill(Color.black) - .frame(width: 60, height: 60) - .overlay( - Image(systemName: "play.fill") - .foregroundColor(.white) - .font(.title2) - ) + SubscriptionStatusBar( + status: .pioneer(expiryDate: Date()) , + onSubscribeTap: { + // 订阅操作 + handleSubscribe() } - .padding(20) - .background(Color.orange.opacity(0.2)) - .cornerRadius(16) - .padding(.horizontal, 20) - } + ) + .padding(.horizontal, Theme.Spacing.xl) } // MARK: - 积分信息 private var creditsSection: some View { - HStack { - Text("Credits: 3290") - .font(Typography.font(for: .body, family: .quicksand)) - .fontWeight(.medium) + VStack(spacing: 16) { + CreditsInfoCard( + totalCredits: 3290, + onInfoTap: { + // 显示积分信息说明 + }, + onDetailTap: { + // 跳转到积分详情页面 + } + ) - Button(action: { - // 积分信息操作 - }) { - Image(systemName: "info.circle") - .foregroundColor(.gray) - } - - Spacer() - - Image(systemName: "chevron.right") - .foregroundColor(.gray) - .font(.caption) } - .padding(.horizontal, 20) - .padding(.vertical, 16) - .background(Color(.systemBackground)) - .cornerRadius(12) - .padding(.horizontal, 20) - .padding(.top, 20) + .padding(.horizontal, Theme.Spacing.xl) + .padding(.top, Theme.Spacing.xl) } // MARK: - 订阅计划选择 private var subscriptionPlansSection: some View { - HStack(spacing: 16) { - // Free 计划 - SubscriptionPlanCard( - plan: .free, - isSelected: selectedPlan == .free, - onTap: { selectedPlan = .free } - ) - - // Pioneer 计划 - SubscriptionPlanCard( - plan: .pioneer, - isSelected: selectedPlan == .pioneer, - onTap: { selectedPlan = .pioneer } - ) - } - .padding(.horizontal, 20) - .padding(.top, 20) + PlanSelector( + selectedPlan: $selectedPlan, + onPlanSelected: { plan in + print("Selected plan: \(plan.displayName)") + } + ) + .padding(.horizontal, Theme.Spacing.xl) + .padding(.top, Theme.Spacing.xl) } // MARK: - 特别优惠横幅 private var specialOfferBanner: some View { - Text("First 100 users get a special deal: just $1 for your first month!") - .font(Typography.font(for: .caption, family: .quicksand)) - .multilineTextAlignment(.center) - .padding(.horizontal, 20) - .padding(.top, 12) - .foregroundColor(.secondary) + HStack(spacing: 0) { + Text("First") + .font(Typography.font(for: .footnote, family: .quicksandRegular)) + .foregroundColor(Theme.Colors.textPrimary) + + Text(" 100") + .font(Typography.font(for: .footnote, family: .quicksandBold)) + .foregroundColor(Theme.Colors.textPrimary) + + Text(" users get a special deal: justs") + .font(Typography.font(for: .footnote, family: .quicksandRegular)) + .foregroundColor(Theme.Colors.textPrimary) + + Text(" $1") + .font(Typography.font(for: .footnote, family: .quicksandBold)) + .foregroundColor(Theme.Colors.textPrimary) + + Text(" for your first month!") + .font(Typography.font(for: .footnote, family: .quicksandRegular)) + .foregroundColor(Theme.Colors.textPrimary) + } + .multilineTextAlignment(.center) + .padding(.horizontal, Theme.Spacing.lg) + .padding(.top, Theme.Spacing.sm) + .padding(.bottom, Theme.Spacing.lg) } // MARK: - 功能对比表 private var featureComparisonTable: some View { - VStack(spacing: 0) { - // 表头 - HStack { - Spacer() - Text("Free") - .font(Typography.font(for: .subtitle, family: .quicksand)) - .fontWeight(.medium) - .frame(maxWidth: .infinity) - .foregroundColor(.gray) - Text("Pro") - .font(Typography.font(for: .subtitle, family: .quicksand)) - .fontWeight(.medium) - .frame(maxWidth: .infinity) - .foregroundColor(.gray) - } - .padding(.vertical, 16) - .background(Color(.systemGray6)) - - // 功能行 - ForEach(Array(features.enumerated()), id: \.offset) { index, feature in - FeatureRow(feature: feature, isLast: index == features.count - 1) - } - } - .background(Color(.systemBackground)) - .cornerRadius(12) - .padding(.horizontal, 20) - .padding(.top, 20) - .shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1) + PlanCompare() + .padding(.horizontal, Theme.Spacing.lg) } // MARK: - 订阅按钮 private var subscribeButton: some View { VStack(spacing: 12) { - Button(action: { - handleSubscribe() - }) { - if isLoading { - HStack { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .scaleEffect(0.8) - Text("Subscribe") - .font(Typography.font(for: .body, family: .quicksand)) - .fontWeight(.semibold) - } - } else { - Text("Subscribe") - .font(Typography.font(for: .body, family: .quicksand)) - .fontWeight(.semibold) - } - } - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .padding(.vertical, 16) - .background(Color.blue) - .cornerRadius(25) - .disabled(isLoading) - - Text("Get 5,000 Permanent Credits") - .font(Typography.font(for: .caption, family: .quicksand)) - .foregroundColor(.secondary) + SubscribeButton( + title: "Subscribe", + isLoading: isLoading, + action: handleSubscribe + ) } - .padding(.horizontal, 20) - .padding(.top, 30) + .padding(.horizontal, Theme.Spacing.xl) + .padding(.top, Theme.Spacing.lg) } // MARK: - 法律链接 private var legalLinks: some View { HStack(spacing: 8) { - Button("Terms of Service") { + Button(action: { // 打开服务条款 + }) { + Text("Terms of Service") + .underline() } Text("|") .foregroundColor(.secondary) - Button("Privacy Policy") { + Button(action: { // 打开隐私政策 + }) { + Text("Privacy Policy") + .underline() } Text("|") .foregroundColor(.secondary) - Button("Restore Purchase") { + Button(action: { // 恢复购买 + }) { + Text("Restore Purchase") + .underline() } } - .font(Typography.font(for: .caption, family: .quicksand)) + .font(Typography.font(for: .caption, family: .quicksandRegular)) .foregroundColor(.secondary) - .padding(.top, 16) + .padding(.top, Theme.Spacing.sm) } // MARK: - 订阅处理 @@ -299,91 +236,6 @@ struct SubscribeView: View { } } -// MARK: - 订阅计划卡片 -struct SubscriptionPlanCard: View { - let plan: SubscriptionPlan - let isSelected: Bool - let onTap: () -> Void - - var body: some View { - Button(action: onTap) { - VStack(spacing: 12) { - if plan.isPopular { - HStack { - Spacer() - Text("Popular") - .font(Typography.font(for: .caption, family: .quicksand)) - .fontWeight(.medium) - .foregroundColor(.white) - .padding(.horizontal, 12) - .padding(.vertical, 4) - .background(Color.black) - .cornerRadius(12) - } - .padding(.top, -8) - } - - Text(plan.displayName) - .font(Typography.font(for: .title, family: .quicksand)) - .fontWeight(.bold) - .foregroundColor(plan == .pioneer ? .white : .gray) - - Text(plan.price) - .font(Typography.font(for: .body, family: .quicksand)) - .fontWeight(.medium) - .foregroundColor(plan == .pioneer ? .white : .gray) - - Spacer() - } - .frame(maxWidth: .infinity, minHeight: 120) - .padding(16) - .background(plan == .pioneer ? Color.orange : Color.white) - .cornerRadius(16) - .overlay( - RoundedRectangle(cornerRadius: 16) - .stroke(isSelected ? Color.blue : (plan == .free ? Color.blue : Color.clear), lineWidth: 2) - ) - .shadow(color: Color.black.opacity(0.1), radius: 4, x: 0, y: 2) - } - } -} - -// MARK: - 功能对比行 -struct FeatureRow: View { - let feature: SubscriptionFeature - let isLast: Bool - - var body: some View { - HStack { - Text(feature.name) - .font(Typography.font(for: .subtitle, family: .quicksand)) - .fontWeight(.medium) - .frame(maxWidth: .infinity, alignment: .leading) - .foregroundColor(.primary) - - Text(feature.freeValue) - .font(Typography.font(for: .caption, family: .quicksand)) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .foregroundColor(.gray) - - Text(feature.proValue) - .font(Typography.font(for: .caption, family: .quicksand)) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .foregroundColor(.gray) - } - .padding(.vertical, 12) - .padding(.horizontal, 16) - .background(Color(.systemBackground)) - - if !isLast { - Divider() - .padding(.leading, 16) - } - } -} - #Preview { SubscribeView() }