feat: 主题色
This commit is contained in:
parent
f361d73bf7
commit
7d40fe3203
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"lldb.library": "/Library/Developer/CommandLineTools/Library/PrivateFrameworks/LLDB.framework/Versions/A/LLDB",
|
||||
"lldb.launch.expressions": "native"
|
||||
}
|
||||
Binary file not shown.
34
wake/Extensions/ColorExtensions.swift
Normal file
34
wake/Extensions/ColorExtensions.swift
Normal file
@ -0,0 +1,34 @@
|
||||
//
|
||||
// ColorExtensions.swift
|
||||
// wake
|
||||
//
|
||||
// Created by fairclip on 2025/8/19.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Color Extension for Hex Colors
|
||||
extension Color {
|
||||
/// 通过十六进制字符串创建颜色
|
||||
/// - Parameter hex: 十六进制颜色字符串 (例如: "FF5733", "FFF8DE")
|
||||
init(hex: String) {
|
||||
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||
var int: UInt64 = 0
|
||||
Scanner(string: hex).scanHexInt64(&int)
|
||||
let a, r, g, b: UInt64
|
||||
switch hex.count {
|
||||
case 6: // RGB (24-bit)
|
||||
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
||||
default:
|
||||
(a, r, g, b) = (1, 1, 1, 0)
|
||||
}
|
||||
|
||||
self.init(
|
||||
.sRGB,
|
||||
red: Double(r) / 255,
|
||||
green: Double(g) / 255,
|
||||
blue: Double(b) / 255,
|
||||
opacity: Double(a) / 255
|
||||
)
|
||||
}
|
||||
}
|
||||
177
wake/Theme.swift
Normal file
177
wake/Theme.swift
Normal file
@ -0,0 +1,177 @@
|
||||
//
|
||||
// Theme.swift
|
||||
// wake
|
||||
//
|
||||
// Created by fairclip on 2025/8/19.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - 主题色管理
|
||||
struct Theme {
|
||||
|
||||
// MARK: - 主要颜色
|
||||
struct Colors {
|
||||
|
||||
// MARK: - 品牌色
|
||||
static let primary = Color(hex: "FFB645") // 主橙色
|
||||
static let primaryLight = Color(hex: "FFF8DE") // 浅橙色
|
||||
static let primaryDark = Color(hex: "E6A03D") // 深橙色
|
||||
|
||||
// MARK: - 辅助色
|
||||
static let secondary = Color(hex: "6C7B7F") // 灰蓝色
|
||||
static let accent = Color(hex: "FF6B6B") // 强调红色
|
||||
|
||||
// MARK: - 中性色
|
||||
static let background = Color(hex: "F8F9FA") // 背景色
|
||||
static let surface = Color.white // 表面色
|
||||
static let surfaceSecondary = Color(hex: "F5F5F5") // 次级表面色
|
||||
|
||||
// MARK: - 文本色
|
||||
static let textPrimary = Color.black // 主文本色
|
||||
static let textSecondary = Color(hex: "6B7280") // 次级文本色
|
||||
static let textTertiary = Color(hex: "9CA3AF") // 三级文本色
|
||||
static let textInverse = Color.white // 反色文本
|
||||
|
||||
// MARK: - 状态色
|
||||
static let success = Color(hex: "10B981") // 成功色
|
||||
static let warning = Color(hex: "F59E0B") // 警告色
|
||||
static let error = Color(hex: "EF4444") // 错误色
|
||||
static let info = Color(hex: "3B82F6") // 信息色
|
||||
|
||||
// MARK: - 边框色
|
||||
static let border = Color(hex: "E5E7EB") // 边框色
|
||||
static let borderLight = Color(hex: "F3F4F6") // 浅边框色
|
||||
static let borderDark = Color(hex: "D1D5DB") // 深边框色
|
||||
|
||||
// MARK: - 订阅相关色
|
||||
static let freeBackground = primaryLight // Free版背景
|
||||
static let pioneerBackground = primary // Pioneer版背景
|
||||
static let subscribeButton = primary // 订阅按钮色
|
||||
}
|
||||
|
||||
// MARK: - 渐变色
|
||||
struct Gradients {
|
||||
static let primaryGradient = LinearGradient(
|
||||
colors: [Colors.primary, Colors.primaryDark],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
|
||||
static let backgroundGradient = LinearGradient(
|
||||
colors: [Colors.background, Colors.surface],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
|
||||
static let accentGradient = LinearGradient(
|
||||
colors: [Colors.accent, Color(hex: "FF8E8E")],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - 阴影
|
||||
struct Shadows {
|
||||
static let small = Color.black.opacity(0.1)
|
||||
static let medium = Color.black.opacity(0.15)
|
||||
static let large = Color.black.opacity(0.2)
|
||||
|
||||
// 阴影配置
|
||||
static let cardShadow = (color: small, radius: CGFloat(4), x: CGFloat(0), y: CGFloat(2))
|
||||
static let buttonShadow = (color: medium, radius: CGFloat(6), x: CGFloat(0), y: CGFloat(3))
|
||||
static let modalShadow = (color: large, radius: CGFloat(12), x: CGFloat(0), y: CGFloat(8))
|
||||
}
|
||||
|
||||
// MARK: - 圆角
|
||||
struct CornerRadius {
|
||||
static let small: CGFloat = 8
|
||||
static let medium: CGFloat = 12
|
||||
static let large: CGFloat = 16
|
||||
static let extraLarge: CGFloat = 20
|
||||
static let round: CGFloat = 50
|
||||
}
|
||||
|
||||
// MARK: - 间距
|
||||
struct Spacing {
|
||||
static let xs: CGFloat = 4
|
||||
static let sm: CGFloat = 8
|
||||
static let md: CGFloat = 12
|
||||
static let lg: CGFloat = 16
|
||||
static let xl: CGFloat = 20
|
||||
static let xxl: CGFloat = 24
|
||||
static let xxxl: CGFloat = 32
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 便捷扩展
|
||||
extension Color {
|
||||
/// 主题色快捷访问
|
||||
static var themePrimary: Color { Theme.Colors.primary }
|
||||
static var themePrimaryLight: Color { Theme.Colors.primaryLight }
|
||||
static var themeSecondary: Color { Theme.Colors.secondary }
|
||||
static var themeAccent: Color { Theme.Colors.accent }
|
||||
static var themeBackground: Color { Theme.Colors.background }
|
||||
static var themeSurface: Color { Theme.Colors.surface }
|
||||
static var themeTextPrimary: Color { Theme.Colors.textPrimary }
|
||||
static var themeTextSecondary: Color { Theme.Colors.textSecondary }
|
||||
}
|
||||
|
||||
// MARK: - 预览
|
||||
#Preview("Theme Colors") {
|
||||
ScrollView {
|
||||
VStack(spacing: Theme.Spacing.lg) {
|
||||
// 品牌色
|
||||
ColorPreviewSection(title: "品牌色", colors: [
|
||||
("Primary", Theme.Colors.primary),
|
||||
("Primary Light", Theme.Colors.primaryLight),
|
||||
("Primary Dark", Theme.Colors.primaryDark)
|
||||
])
|
||||
|
||||
// 辅助色
|
||||
ColorPreviewSection(title: "辅助色", colors: [
|
||||
("Secondary", Theme.Colors.secondary),
|
||||
("Accent", Theme.Colors.accent)
|
||||
])
|
||||
|
||||
// 状态色
|
||||
ColorPreviewSection(title: "状态色", colors: [
|
||||
("Success", Theme.Colors.success),
|
||||
("Warning", Theme.Colors.warning),
|
||||
("Error", Theme.Colors.error),
|
||||
("Info", Theme.Colors.info)
|
||||
])
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(Theme.Colors.background)
|
||||
}
|
||||
|
||||
// MARK: - 预览辅助组件
|
||||
struct ColorPreviewSection: View {
|
||||
let title: String
|
||||
let colors: [(String, Color)]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.foregroundColor(Theme.Colors.textPrimary)
|
||||
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: Theme.Spacing.sm) {
|
||||
ForEach(colors, id: \.0) { name, color in
|
||||
VStack(spacing: Theme.Spacing.xs) {
|
||||
Rectangle()
|
||||
.fill(color)
|
||||
.frame(height: 60)
|
||||
.cornerRadius(Theme.CornerRadius.small)
|
||||
|
||||
Text(name)
|
||||
.font(.caption)
|
||||
.foregroundColor(Theme.Colors.textSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
140
wake/View/Subscribe/Components/SubscriptionStatusBar.swift
Normal file
140
wake/View/Subscribe/Components/SubscriptionStatusBar.swift
Normal file
@ -0,0 +1,140 @@
|
||||
//
|
||||
// SubscriptionStatusBar.swift
|
||||
// wake
|
||||
//
|
||||
// Created by fairclip on 2025/8/19.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - 订阅状态枚举
|
||||
enum SubscriptionStatus {
|
||||
case free
|
||||
case pioneer(expiryDate: Date)
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .free:
|
||||
return "Free"
|
||||
case .pioneer:
|
||||
return "Pioneer"
|
||||
}
|
||||
}
|
||||
|
||||
var hasExpiry: Bool {
|
||||
switch self {
|
||||
case .free:
|
||||
return false
|
||||
case .pioneer:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
var backgroundColor: Color {
|
||||
switch self {
|
||||
case .free:
|
||||
return Theme.Colors.freeBackground // 浅橙色背景
|
||||
case .pioneer:
|
||||
return Theme.Colors.pioneerBackground // 橙色背景
|
||||
}
|
||||
}
|
||||
|
||||
var textColor: Color {
|
||||
switch self {
|
||||
case .free:
|
||||
return Theme.Colors.textPrimary
|
||||
case .pioneer:
|
||||
return Theme.Colors.textPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 订阅状态栏组件
|
||||
struct SubscriptionStatusBar: View {
|
||||
let status: SubscriptionStatus
|
||||
let onSubscribeTap: (() -> Void)?
|
||||
|
||||
init(status: SubscriptionStatus, onSubscribeTap: (() -> Void)? = nil) {
|
||||
self.status = status
|
||||
self.onSubscribeTap = onSubscribeTap
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
// 订阅类型标题
|
||||
Text(status.title)
|
||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||
.foregroundColor(status.textColor)
|
||||
|
||||
// 过期时间或订阅按钮
|
||||
if case .pioneer(let expiryDate) = status {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Expires on :")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(status.textColor.opacity(0.8))
|
||||
|
||||
Text(formatDate(expiryDate))
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundColor(status.textColor)
|
||||
}
|
||||
} else {
|
||||
Button(action: {
|
||||
onSubscribeTap?()
|
||||
}) {
|
||||
Text("Subscribe")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundColor(Theme.Colors.textPrimary)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 8)
|
||||
.background(Theme.Colors.subscribeButton)
|
||||
.cornerRadius(Theme.CornerRadius.large)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// 播放按钮图标
|
||||
Circle()
|
||||
.fill(Color.black)
|
||||
.frame(width: 60, height: 60)
|
||||
.overlay(
|
||||
Image(systemName: "play.fill")
|
||||
.foregroundColor(.white)
|
||||
.font(.title2)
|
||||
.offset(x: 2) // 微调播放图标位置
|
||||
)
|
||||
}
|
||||
.padding(20)
|
||||
.background(status.backgroundColor)
|
||||
.cornerRadius(20)
|
||||
.shadow(color: Color.black.opacity(0.1), radius: 4, x: 0, y: 2)
|
||||
}
|
||||
|
||||
// MARK: - 日期格式化
|
||||
private func formatDate(_ date: Date) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "MMMM d, yyyy"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 预览
|
||||
#Preview("Free Status") {
|
||||
VStack(spacing: 20) {
|
||||
SubscriptionStatusBar(
|
||||
status: .free,
|
||||
onSubscribeTap: {
|
||||
print("Subscribe tapped")
|
||||
}
|
||||
)
|
||||
.padding()
|
||||
|
||||
SubscriptionStatusBar(
|
||||
status: .pioneer(expiryDate: Calendar.current.date(byAdding: .month, value: 6, to: Date()) ?? Date())
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user