Compare commits
2 Commits
c293b248d0
...
805fa0c256
| Author | SHA1 | Date | |
|---|---|---|---|
| 805fa0c256 | |||
| 4be13a7141 |
Binary file not shown.
@ -1 +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)
|
||||||
|
}
|
||||||
69
wake/View/Subscribe/Components/SubscribeButton.swift
Normal file
69
wake/View/Subscribe/Components/SubscribeButton.swift
Normal file
@ -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)
|
||||||
|
}
|
||||||
@ -71,8 +71,8 @@ struct SubscribeView: View {
|
|||||||
}
|
}
|
||||||
.background(Theme.Colors.cardBackground)
|
.background(Theme.Colors.cardBackground)
|
||||||
.cornerRadius(Theme.CornerRadius.medium)
|
.cornerRadius(Theme.CornerRadius.medium)
|
||||||
.padding(.horizontal, Theme.Spacing.xl)
|
.padding(.horizontal, Theme.Spacing.lg)
|
||||||
.padding(.vertical, Theme.Spacing.xl)
|
.padding(.vertical, Theme.Spacing.lg)
|
||||||
|
|
||||||
|
|
||||||
// 功能对比表
|
// 功能对比表
|
||||||
@ -172,96 +172,56 @@ struct SubscribeView: View {
|
|||||||
|
|
||||||
// MARK: - 功能对比表
|
// MARK: - 功能对比表
|
||||||
private var featureComparisonTable: some View {
|
private var featureComparisonTable: some View {
|
||||||
VStack(spacing: 0) {
|
PlanCompare()
|
||||||
// 表头
|
.padding(.horizontal, Theme.Spacing.lg)
|
||||||
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: - 订阅按钮
|
// MARK: - 订阅按钮
|
||||||
private var subscribeButton: some View {
|
private var subscribeButton: some View {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
Button(action: {
|
SubscribeButton(
|
||||||
handleSubscribe()
|
title: "Subscribe",
|
||||||
}) {
|
isLoading: isLoading,
|
||||||
if isLoading {
|
action: handleSubscribe
|
||||||
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, 20)
|
.padding(.horizontal, Theme.Spacing.xl)
|
||||||
.padding(.top, 30)
|
.padding(.top, Theme.Spacing.lg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 法律链接
|
// MARK: - 法律链接
|
||||||
private var legalLinks: some View {
|
private var legalLinks: some View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Button("Terms of Service") {
|
Button(action: {
|
||||||
// 打开服务条款
|
// 打开服务条款
|
||||||
|
}) {
|
||||||
|
Text("Terms of Service")
|
||||||
|
.underline()
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("|")
|
Text("|")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
Button("Privacy Policy") {
|
Button(action: {
|
||||||
// 打开隐私政策
|
// 打开隐私政策
|
||||||
|
}) {
|
||||||
|
Text("Privacy Policy")
|
||||||
|
.underline()
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("|")
|
Text("|")
|
||||||
.foregroundColor(.secondary)
|
.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)
|
.foregroundColor(.secondary)
|
||||||
.padding(.top, 16)
|
.padding(.top, Theme.Spacing.sm)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 订阅处理
|
// MARK: - 订阅处理
|
||||||
@ -276,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 {
|
#Preview {
|
||||||
SubscribeView()
|
SubscribeView()
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user