Compare commits
2 Commits
805fa0c256
...
c293b248d0
| Author | SHA1 | Date | |
|---|---|---|---|
| c293b248d0 | |||
| 417e101333 |
Binary file not shown.
@ -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)
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -57,7 +57,7 @@ struct SubscribeView: View {
|
|||||||
navigationHeader
|
navigationHeader
|
||||||
|
|
||||||
// 当前订阅状态卡片
|
// 当前订阅状态卡片
|
||||||
currentSubscriptionCard
|
currentSubscriptionCard
|
||||||
|
|
||||||
// 积分信息
|
// 积分信息
|
||||||
creditsSection
|
creditsSection
|
||||||
@ -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.lg)
|
.padding(.horizontal, Theme.Spacing.xl)
|
||||||
.padding(.vertical, Theme.Spacing.lg)
|
.padding(.vertical, Theme.Spacing.xl)
|
||||||
|
|
||||||
|
|
||||||
// 功能对比表
|
// 功能对比表
|
||||||
@ -172,56 +172,96 @@ struct SubscribeView: View {
|
|||||||
|
|
||||||
// MARK: - 功能对比表
|
// MARK: - 功能对比表
|
||||||
private var featureComparisonTable: some View {
|
private var featureComparisonTable: some View {
|
||||||
PlanCompare()
|
VStack(spacing: 0) {
|
||||||
.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) {
|
||||||
SubscribeButton(
|
Button(action: {
|
||||||
title: "Subscribe",
|
handleSubscribe()
|
||||||
isLoading: isLoading,
|
}) {
|
||||||
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(.horizontal, 20)
|
||||||
.padding(.top, Theme.Spacing.lg)
|
.padding(.top, 30)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 法律链接
|
// MARK: - 法律链接
|
||||||
private var legalLinks: some View {
|
private var legalLinks: some View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Button(action: {
|
Button("Terms of Service") {
|
||||||
// 打开服务条款
|
// 打开服务条款
|
||||||
}) {
|
|
||||||
Text("Terms of Service")
|
|
||||||
.underline()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("|")
|
Text("|")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
Button(action: {
|
Button("Privacy Policy") {
|
||||||
// 打开隐私政策
|
// 打开隐私政策
|
||||||
}) {
|
|
||||||
Text("Privacy Policy")
|
|
||||||
.underline()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("|")
|
Text("|")
|
||||||
.foregroundColor(.secondary)
|
.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)
|
.foregroundColor(.secondary)
|
||||||
.padding(.top, Theme.Spacing.sm)
|
.padding(.top, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 订阅处理
|
// 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 {
|
#Preview {
|
||||||
SubscribeView()
|
SubscribeView()
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user