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: - 功能对比表