feat: 订阅页面
This commit is contained in:
parent
7d40fe3203
commit
62c2defb1d
Binary file not shown.
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Bucket
|
||||||
|
uuid = "55F37A93-4556-4005-B9BD-8F1A1D6A8474"
|
||||||
|
type = "1"
|
||||||
|
version = "2.0">
|
||||||
|
<Breakpoints>
|
||||||
|
<BreakpointProxy
|
||||||
|
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||||
|
<BreakpointContent
|
||||||
|
uuid = "F1BBF7E2-4D6E-4646-83BC-F57E600056E4"
|
||||||
|
shouldBeEnabled = "Yes"
|
||||||
|
ignoreCount = "0"
|
||||||
|
continueAfterRunningActions = "No"
|
||||||
|
filePath = "wake/View/Subscribe/SubscribeView.swift"
|
||||||
|
startingColumnNumber = "9223372036854775807"
|
||||||
|
endingColumnNumber = "9223372036854775807"
|
||||||
|
startingLineNumber = "65"
|
||||||
|
endingLineNumber = "65"
|
||||||
|
landmarkName = "body"
|
||||||
|
landmarkType = "24">
|
||||||
|
</BreakpointContent>
|
||||||
|
</BreakpointProxy>
|
||||||
|
</Breakpoints>
|
||||||
|
</Bucket>
|
||||||
@ -23,7 +23,7 @@ struct Theme {
|
|||||||
static let accent = Color(hex: "FF6B6B") // 强调红色
|
static let accent = Color(hex: "FF6B6B") // 强调红色
|
||||||
|
|
||||||
// MARK: - 中性色
|
// MARK: - 中性色
|
||||||
static let background = Color(hex: "F8F9FA") // 背景色
|
static let background = Color(hex: "FAFAFA") // 背景色
|
||||||
static let surface = Color.white // 表面色
|
static let surface = Color.white // 表面色
|
||||||
static let surfaceSecondary = Color(hex: "F5F5F5") // 次级表面色
|
static let surfaceSecondary = Color(hex: "F5F5F5") // 次级表面色
|
||||||
|
|
||||||
@ -40,14 +40,18 @@ struct Theme {
|
|||||||
static let info = Color(hex: "3B82F6") // 信息色
|
static let info = Color(hex: "3B82F6") // 信息色
|
||||||
|
|
||||||
// MARK: - 边框色
|
// MARK: - 边框色
|
||||||
static let border = Color(hex: "E5E7EB") // 边框色
|
static let border = Color(hex: "D9D9D9") // 边框色
|
||||||
static let borderLight = Color(hex: "F3F4F6") // 浅边框色
|
static let borderLight = Color(hex: "F3F4F6") // 浅边框色
|
||||||
static let borderDark = Color(hex: "D1D5DB") // 深边框色
|
static let borderBlack = Color.black // 黑色边框色
|
||||||
|
static let borderDark = borderBlack // 深边框色
|
||||||
|
|
||||||
// MARK: - 订阅相关色
|
// MARK: - 订阅相关色
|
||||||
static let freeBackground = primaryLight // Free版背景
|
static let freeBackground = primaryLight // Free版背景
|
||||||
static let pioneerBackground = primary // Pioneer版背景
|
static let pioneerBackground = primary // Pioneer版背景
|
||||||
static let subscribeButton = primary // 订阅按钮色
|
static let subscribeButton = primary // 订阅按钮色
|
||||||
|
|
||||||
|
// MARK: - 卡片相关色
|
||||||
|
static let cardBackground = Color.white // 卡片背景
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 渐变色
|
// MARK: - 渐变色
|
||||||
@ -59,9 +63,13 @@ struct Theme {
|
|||||||
)
|
)
|
||||||
|
|
||||||
static let backgroundGradient = LinearGradient(
|
static let backgroundGradient = LinearGradient(
|
||||||
colors: [Colors.background, Colors.surface],
|
gradient: Gradient(colors: [
|
||||||
startPoint: .top,
|
Color(hex: "FBC063"),
|
||||||
endPoint: .bottom
|
Color(hex: "FEE9BE"),
|
||||||
|
Color(hex: "FAB851")
|
||||||
|
]),
|
||||||
|
startPoint: .topLeading,
|
||||||
|
endPoint: .bottomTrailing
|
||||||
)
|
)
|
||||||
|
|
||||||
static let accentGradient = LinearGradient(
|
static let accentGradient = LinearGradient(
|
||||||
@ -69,6 +77,12 @@ struct Theme {
|
|||||||
startPoint: .leading,
|
startPoint: .leading,
|
||||||
endPoint: .trailing
|
endPoint: .trailing
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// static let creditsInfoTooltip = LinearGradient(
|
||||||
|
// colors: [Colors(hex: "FFD38F"), Colors(hex: "FFF8DE"), Colors(hex: "FECE83")],
|
||||||
|
// startPoint: .topLeading,
|
||||||
|
// endPoint: .bottomTrailing
|
||||||
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 阴影
|
// MARK: - 阴影
|
||||||
|
|||||||
@ -60,21 +60,21 @@ struct Typography {
|
|||||||
/// - style: 文本样式
|
/// - style: 文本样式
|
||||||
/// - family: 字体库,默认为 nil 使用默认字体库
|
/// - family: 字体库,默认为 nil 使用默认字体库
|
||||||
/// - Returns: 配置好的 Font 对象
|
/// - 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
|
let fontFamily = family ?? defaultFontFamily
|
||||||
guard let config = styleConfig[style] else {
|
guard let config = styleConfig[style] else {
|
||||||
return .body
|
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 metrics = UIFontMetrics(forTextStyle: config.textStyle)
|
||||||
let scaledFont = metrics.scaledFont(for: customFont)
|
let scaledFont = metrics.scaledFont(for: customFont)
|
||||||
return Font(scaledFont)
|
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 metrics = UIFontMetrics(forTextStyle: config.textStyle)
|
||||||
let scaledFont = metrics.scaledFont(for: systemFont)
|
let scaledFont = metrics.scaledFont(for: systemFont)
|
||||||
return Font(scaledFont)
|
return Font(scaledFont)
|
||||||
|
|||||||
288
wake/View/Credits/CreditsDetailView.swift
Normal file
288
wake/View/Credits/CreditsDetailView.swift
Normal file
@ -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()
|
||||||
|
}
|
||||||
107
wake/View/Credits/CreditsInfoCard.swift
Normal file
107
wake/View/Credits/CreditsInfoCard.swift
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
//
|
||||||
|
// CreditsInfoCard.swift
|
||||||
|
// wake
|
||||||
|
//
|
||||||
|
// Created by fairclip on 2025/8/19.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// MARK: - 积分信息卡片组件
|
||||||
|
struct CreditsInfoCard: View {
|
||||||
|
let totalCredits: Int
|
||||||
|
let onInfoTap: (() -> Void)?
|
||||||
|
let onDetailTap: (() -> Void)?
|
||||||
|
|
||||||
|
@State private var showInfoPopover = false
|
||||||
|
|
||||||
|
init(
|
||||||
|
totalCredits: Int,
|
||||||
|
onInfoTap: (() -> Void)? = nil,
|
||||||
|
onDetailTap: (() -> Void)? = nil
|
||||||
|
) {
|
||||||
|
self.totalCredits = totalCredits
|
||||||
|
self.onInfoTap = onInfoTap
|
||||||
|
self.onDetailTap = onDetailTap
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: {
|
||||||
|
onDetailTap?()
|
||||||
|
}) {
|
||||||
|
mainCreditsSection
|
||||||
|
}
|
||||||
|
.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.md) {
|
||||||
|
// 积分图标和数量
|
||||||
|
HStack(spacing: Theme.Spacing.sm) {
|
||||||
|
Text("Credits:")
|
||||||
|
.font(Typography.font(for: .subtitle, family: .quicksandBold))
|
||||||
|
.foregroundColor(Theme.Colors.textPrimary)
|
||||||
|
|
||||||
|
Text("\(totalCredits)")
|
||||||
|
.font(Typography.font(for: .subtitle, family: .quicksandBold))
|
||||||
|
.foregroundColor(Theme.Colors.textPrimary)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 操作按钮区域
|
||||||
|
HStack(spacing: Theme.Spacing.sm) {
|
||||||
|
// 信息按钮
|
||||||
|
Button(action: {
|
||||||
|
showInfoPopover = true
|
||||||
|
onInfoTap?()
|
||||||
|
}) {
|
||||||
|
Image(systemName: "questionmark.circle")
|
||||||
|
.foregroundColor(Theme.Colors.textSecondary)
|
||||||
|
.font(.system(size: 16))
|
||||||
|
}
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
// 详情按钮
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.foregroundColor(Theme.Colors.textPrimary)
|
||||||
|
.font(.system(size: 14, weight: .medium))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(Theme.Spacing.lg)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - 预览
|
||||||
|
#Preview("Credits Info Card") {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
CreditsInfoCard(
|
||||||
|
totalCredits: 3290,
|
||||||
|
onInfoTap: {
|
||||||
|
print("Info tapped")
|
||||||
|
},
|
||||||
|
onDetailTap: {
|
||||||
|
print("Detail tapped")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemGroupedBackground))
|
||||||
|
}
|
||||||
165
wake/View/Subscribe/Components/PlanCompare.swift
Normal file
165
wake/View/Subscribe/Components/PlanCompare.swift
Normal file
@ -0,0 +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)
|
||||||
|
}
|
||||||
135
wake/View/Subscribe/Components/PlanSelector.swift
Normal file
135
wake/View/Subscribe/Components/PlanSelector.swift
Normal file
@ -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<SubscriptionPlan?>,
|
||||||
|
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))
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
@ -61,21 +61,21 @@ struct SubscriptionStatusBar: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 16) {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
// 订阅类型标题
|
// 订阅类型标题
|
||||||
Text(status.title)
|
Text(status.title)
|
||||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
.font(Typography.font(for: .headline, family: .quicksandBold, size: 32))
|
||||||
.foregroundColor(status.textColor)
|
.foregroundColor(status.textColor)
|
||||||
|
|
||||||
// 过期时间或订阅按钮
|
// 过期时间或订阅按钮
|
||||||
if case .pioneer(let expiryDate) = status {
|
if case .pioneer(let expiryDate) = status {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("Expires on :")
|
Text("Expires on :")
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(Typography.font(for: .body, family: .quicksandRegular))
|
||||||
.foregroundColor(status.textColor.opacity(0.8))
|
.foregroundColor(status.textColor.opacity(0.7))
|
||||||
|
|
||||||
Text(formatDate(expiryDate))
|
Text(formatDate(expiryDate))
|
||||||
.font(.system(size: 16, weight: .semibold))
|
.font(Typography.font(for: .body, family: .quicksandRegular))
|
||||||
.foregroundColor(status.textColor)
|
.foregroundColor(status.textColor)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -83,12 +83,12 @@ struct SubscriptionStatusBar: View {
|
|||||||
onSubscribeTap?()
|
onSubscribeTap?()
|
||||||
}) {
|
}) {
|
||||||
Text("Subscribe")
|
Text("Subscribe")
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(Typography.font(for: .title, family: .quicksandRegular, size: 16))
|
||||||
.foregroundColor(Theme.Colors.textPrimary)
|
.foregroundColor(Theme.Colors.textPrimary)
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 6)
|
||||||
.background(Theme.Colors.subscribeButton)
|
.background(Theme.Gradients.backgroundGradient)
|
||||||
.cornerRadius(Theme.CornerRadius.large)
|
.cornerRadius(Theme.CornerRadius.extraLarge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ struct SubscriptionFeature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct SubscribeView: View {
|
struct SubscribeView: View {
|
||||||
@State private var selectedPlan: SubscriptionPlan = .free
|
@State private var selectedPlan: SubscriptionPlan? = .pioneer
|
||||||
@State private var isLoading = false
|
@State private var isLoading = false
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
|
||||||
@ -57,16 +57,23 @@ struct SubscribeView: View {
|
|||||||
navigationHeader
|
navigationHeader
|
||||||
|
|
||||||
// 当前订阅状态卡片
|
// 当前订阅状态卡片
|
||||||
currentSubscriptionCard
|
currentSubscriptionCard
|
||||||
|
|
||||||
// 积分信息
|
// 积分信息
|
||||||
creditsSection
|
creditsSection
|
||||||
|
|
||||||
// 订阅计划选择
|
VStack {
|
||||||
subscriptionPlansSection
|
// 订阅计划选择
|
||||||
|
subscriptionPlansSection
|
||||||
// 特别优惠提示
|
|
||||||
specialOfferBanner
|
// 特别优惠提示
|
||||||
|
specialOfferBanner
|
||||||
|
}
|
||||||
|
.background(Theme.Colors.cardBackground)
|
||||||
|
.cornerRadius(Theme.CornerRadius.medium)
|
||||||
|
.padding(.horizontal, Theme.Spacing.lg)
|
||||||
|
.padding(.vertical, Theme.Spacing.lg)
|
||||||
|
|
||||||
|
|
||||||
// 功能对比表
|
// 功能对比表
|
||||||
featureComparisonTable
|
featureComparisonTable
|
||||||
@ -80,7 +87,7 @@ struct SubscribeView: View {
|
|||||||
Spacer(minLength: 100)
|
Spacer(minLength: 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(Color(.systemGroupedBackground))
|
.background(Theme.Colors.background)
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,197 +101,127 @@ struct SubscribeView: View {
|
|||||||
|
|
||||||
// MARK: - 当前订阅状态卡片
|
// MARK: - 当前订阅状态卡片
|
||||||
private var currentSubscriptionCard: some View {
|
private var currentSubscriptionCard: some View {
|
||||||
VStack(spacing: 0) {
|
SubscriptionStatusBar(
|
||||||
HStack {
|
status: .pioneer(expiryDate: Date()) ,
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
onSubscribeTap: {
|
||||||
Text("Free")
|
// 订阅操作
|
||||||
.font(Typography.font(for: .headline, family: .quicksand))
|
handleSubscribe()
|
||||||
.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)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.padding(20)
|
)
|
||||||
.background(Color.orange.opacity(0.2))
|
.padding(.horizontal, Theme.Spacing.xl)
|
||||||
.cornerRadius(16)
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 积分信息
|
// MARK: - 积分信息
|
||||||
private var creditsSection: some View {
|
private var creditsSection: some View {
|
||||||
HStack {
|
VStack(spacing: 16) {
|
||||||
Text("Credits: 3290")
|
CreditsInfoCard(
|
||||||
.font(Typography.font(for: .body, family: .quicksand))
|
totalCredits: 3290,
|
||||||
.fontWeight(.medium)
|
onInfoTap: {
|
||||||
|
// 显示积分信息说明
|
||||||
|
},
|
||||||
|
onDetailTap: {
|
||||||
|
// 跳转到积分详情页面
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
// 积分信息操作
|
|
||||||
}) {
|
|
||||||
Image(systemName: "info.circle")
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Image(systemName: "chevron.right")
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
.font(.caption)
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, Theme.Spacing.xl)
|
||||||
.padding(.vertical, 16)
|
.padding(.top, Theme.Spacing.xl)
|
||||||
.background(Color(.systemBackground))
|
|
||||||
.cornerRadius(12)
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.top, 20)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 订阅计划选择
|
// MARK: - 订阅计划选择
|
||||||
private var subscriptionPlansSection: some View {
|
private var subscriptionPlansSection: some View {
|
||||||
HStack(spacing: 16) {
|
PlanSelector(
|
||||||
// Free 计划
|
selectedPlan: $selectedPlan,
|
||||||
SubscriptionPlanCard(
|
onPlanSelected: { plan in
|
||||||
plan: .free,
|
print("Selected plan: \(plan.displayName)")
|
||||||
isSelected: selectedPlan == .free,
|
}
|
||||||
onTap: { selectedPlan = .free }
|
)
|
||||||
)
|
.padding(.horizontal, Theme.Spacing.xl)
|
||||||
|
.padding(.top, Theme.Spacing.xl)
|
||||||
// Pioneer 计划
|
|
||||||
SubscriptionPlanCard(
|
|
||||||
plan: .pioneer,
|
|
||||||
isSelected: selectedPlan == .pioneer,
|
|
||||||
onTap: { selectedPlan = .pioneer }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.top, 20)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 特别优惠横幅
|
// MARK: - 特别优惠横幅
|
||||||
private var specialOfferBanner: some View {
|
private var specialOfferBanner: some View {
|
||||||
Text("First 100 users get a special deal: just $1 for your first month!")
|
HStack(spacing: 0) {
|
||||||
.font(Typography.font(for: .caption, family: .quicksand))
|
Text("First")
|
||||||
.multilineTextAlignment(.center)
|
.font(Typography.font(for: .footnote, family: .quicksandRegular))
|
||||||
.padding(.horizontal, 20)
|
.foregroundColor(Theme.Colors.textPrimary)
|
||||||
.padding(.top, 12)
|
|
||||||
.foregroundColor(.secondary)
|
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: - 功能对比表
|
// 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: - 订阅处理
|
||||||
@ -299,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