Compare commits

..

2 Commits

Author SHA1 Message Date
c293b248d0 feat: 订阅页面 2025-08-20 10:39:06 +08:00
417e101333 feat: 订阅页面一半完成 2025-08-19 20:53:57 +08:00
5 changed files with 152 additions and 260 deletions

View File

@ -1,165 +1 @@
//
// 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)
}

View File

@ -1,69 +0,0 @@
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)
}

View File

@ -57,7 +57,7 @@ struct SubscribeView: View {
navigationHeader
//
currentSubscriptionCard
currentSubscriptionCard
//
creditsSection
@ -71,8 +71,8 @@ struct SubscribeView: View {
}
.background(Theme.Colors.cardBackground)
.cornerRadius(Theme.CornerRadius.medium)
.padding(.horizontal, Theme.Spacing.lg)
.padding(.vertical, Theme.Spacing.lg)
.padding(.horizontal, Theme.Spacing.xl)
.padding(.vertical, Theme.Spacing.xl)
//
@ -172,56 +172,96 @@ struct SubscribeView: View {
// MARK: -
private var featureComparisonTable: some View {
PlanCompare()
.padding(.horizontal, Theme.Spacing.lg)
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)
}
// MARK: -
private var subscribeButton: some View {
VStack(spacing: 12) {
SubscribeButton(
title: "Subscribe",
isLoading: isLoading,
action: handleSubscribe
)
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)
}
.padding(.horizontal, Theme.Spacing.xl)
.padding(.top, Theme.Spacing.lg)
.padding(.horizontal, 20)
.padding(.top, 30)
}
// MARK: -
private var legalLinks: some View {
HStack(spacing: 8) {
Button(action: {
Button("Terms of Service") {
//
}) {
Text("Terms of Service")
.underline()
}
Text("|")
.foregroundColor(.secondary)
Button(action: {
Button("Privacy Policy") {
//
}) {
Text("Privacy Policy")
.underline()
}
Text("|")
.foregroundColor(.secondary)
Button(action: {
Button("Restore Purchase") {
//
}) {
Text("Restore Purchase")
.underline()
}
}
.font(Typography.font(for: .caption, family: .quicksandRegular))
.font(Typography.font(for: .caption, family: .quicksand))
.foregroundColor(.secondary)
.padding(.top, Theme.Spacing.sm)
.padding(.top, 16)
}
// MARK: -
@ -236,6 +276,91 @@ 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()
}