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()
}