diff --git a/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate b/wake.xcodeproj/project.xcworkspace/xcuserdata/fairclip.xcuserdatad/UserInterfaceState.xcuserstate index 6e6e724..b5dc67c 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 index 1ec5010..b374a4c 100644 --- a/wake/View/Credits/CreditsInfoCard.swift +++ b/wake/View/Credits/CreditsInfoCard.swift @@ -1,4 +1,4 @@ -// + // // CreditsInfoCard.swift // wake // @@ -7,294 +7,93 @@ 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 + @State private var showInfoPopover = 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) { - // 主要积分显示区域 + Button(action: { + onDetailTap?() + }) { mainCreditsSection - - // 积分明细展开区域 - if showBreakdown && !creditBreakdown.isEmpty { - creditsBreakdownSection - } } - .background(Color(.systemBackground)) - .cornerRadius(Theme.CornerRadius.medium) + .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.lg) { + HStack(spacing: Theme.Spacing.md) { // 积分图标和数量 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)) - ) + Text("Credits:") + .font(Typography.font(for: .subtitle, family: .quicksandBold)) + .foregroundColor(Theme.Colors.textPrimary) - 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) - } + Text("\(totalCredits)") + .font(Typography.font(for: .subtitle, family: .quicksandBold)) + .foregroundColor(Theme.Colors.textPrimary) } - Spacer() // 操作按钮区域 HStack(spacing: Theme.Spacing.sm) { // 信息按钮 Button(action: { + showInfoPopover = true onInfoTap?() }) { - Image(systemName: "info.circle") + Image(systemName: "questionmark.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)) - } + .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) } - - // 详情按钮 - 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) + // 详情按钮 + Image(systemName: "chevron.right") + .foregroundColor(Theme.Colors.textPrimary) + .font(.system(size: 14, weight: .medium)) } } .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") }, @@ -302,12 +101,6 @@ struct UsageStatItem: View { print("Detail tapped") } ) - - CreditsUsageCard( - todayUsed: 45, - weeklyUsed: 280, - monthlyUsed: 1150 - ) } .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..0519ecb --- /dev/null +++ b/wake/View/Subscribe/Components/PlanCompare.swift @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/wake/View/Subscribe/Components/PlanSelector.swift b/wake/View/Subscribe/Components/PlanSelector.swift index e69de29..d37fe54 100644 --- a/wake/View/Subscribe/Components/PlanSelector.swift +++ 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/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..3b5be88 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 @@ -62,11 +62,18 @@ struct SubscribeView: View { // 积分信息 creditsSection - // 订阅计划选择 - subscriptionPlansSection - - // 特别优惠提示 - specialOfferBanner + VStack { + // 订阅计划选择 + subscriptionPlansSection + + // 特别优惠提示 + specialOfferBanner + } + .background(Theme.Colors.cardBackground) + .cornerRadius(Theme.CornerRadius.medium) + .padding(.horizontal, Theme.Spacing.xl) + .padding(.vertical, Theme.Spacing.xl) + // 功能对比表 featureComparisonTable @@ -80,7 +87,7 @@ struct SubscribeView: View { Spacer(minLength: 100) } } - .background(Color(.systemGroupedBackground)) + .background(Theme.Colors.background) .navigationBarHidden(true) } } @@ -94,103 +101,73 @@ 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: - 功能对比表