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)
|
||||
}
|
||||
@ -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.xl)
|
||||
.padding(.vertical, Theme.Spacing.xl)
|
||||
.padding(.horizontal, Theme.Spacing.lg)
|
||||
.padding(.vertical, Theme.Spacing.lg)
|
||||
|
||||
|
||||
// 功能对比表
|
||||
@ -172,96 +172,56 @@ struct SubscribeView: View {
|
||||
|
||||
// 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: - 订阅处理
|
||||
@ -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 {
|
||||
SubscribeView()
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user